diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index bcdab43..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { defineConfig } from 'eslint/config'; -import typescriptEslint from '@typescript-eslint/eslint-plugin'; -import prettier from 'eslint-plugin-prettier'; -import globals from 'globals'; -import tsParser from '@typescript-eslint/parser'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import js from '@eslint/js'; -import { FlatCompat } from '@eslint/eslintrc'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all, -}); - -export default defineConfig([ - { - extends: compat.extends( - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ), - - plugins: { - '@typescript-eslint': typescriptEslint, - prettier, - }, - - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - - parser: tsParser, - ecmaVersion: 2020, - sourceType: 'module', - }, - - rules: { - 'prettier/prettier': 'error', - - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - argsIgnorePattern: '^_', - }, - ], - }, - }, -]); diff --git a/eslint.config.mts b/eslint.config.mts new file mode 100644 index 0000000..fd42c62 --- /dev/null +++ b/eslint.config.mts @@ -0,0 +1,30 @@ +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsparser from "@typescript-eslint/parser"; +import prettierPlugin from "eslint-plugin-prettier"; +import prettierConfig from "eslint-config-prettier"; + +export default [ + { + files: ["**/*.ts"], + + languageOptions: { + parser: tsparser, + sourceType: "module", + }, + + plugins: { + "@typescript-eslint": tseslint, + prettier: prettierPlugin, + }, + + rules: { + ...tseslint.configs.recommended.rules, + ...prettierConfig.rules, + "@typescript-eslint/no-unused-vars": "warn", + "no-console": "warn", + "prettier/prettier": "error", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + }, + }, +]; \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 16bb3e4..89f6f84 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,6 +16,7 @@ module.exports = { }, ], }, + modulePathIgnorePatterns: ['src/database'], collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.test.ts', diff --git a/package-lock.json b/package-lock.json index 35b2b2b..f1d3d61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/express": "^4.17.21", "@types/jest": "^30.0.0", "@types/node": "^20.14.12", + "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^8.40.0", "@typescript-eslint/parser": "^8.40.0", "esbuild": "^0.25.9", @@ -33,6 +34,7 @@ "jest": "^30.0.5", "jest-sonar-reporter": "^2.0.0", "prettier": "^3.6.2", + "supertest": "^7.1.4", "ts-jest": "^29.4.1", "tsx": "^4.16.2", "typescript": "^5.5.4" @@ -1955,6 +1957,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2025,6 +2040,16 @@ "node": ">=8.0.0" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2143,6 +2168,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2222,6 +2254,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2289,6 +2328,30 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2881,6 +2944,20 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, "node_modules/babel-jest": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", @@ -3392,6 +3469,19 @@ "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -3401,6 +3491,16 @@ "node": ">=14" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3457,6 +3557,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3533,6 +3640,16 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -3577,6 +3694,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3725,6 +3853,22 @@ "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==", + "dev": true, + "license": "MIT", + "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/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -4223,6 +4367,13 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -4352,6 +4503,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4639,6 +4825,22 @@ "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -7826,6 +8028,95 @@ "node": ">=0.10.0" } }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 16cf135..d031123 100644 --- a/package.json +++ b/package.json @@ -30,22 +30,26 @@ "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-resolve": "^0.2.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.34.0", + "@eslint/js": "^9.35.0", "@types/express": "^4.17.21", "@types/jest": "^30.0.0", "@types/node": "^20.14.12", + "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^8.40.0", "@typescript-eslint/parser": "^8.40.0", "esbuild": "^0.25.9", - "eslint": "^9.34.0", + "eslint": "^9.35.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "globals": "^16.3.0", "jest": "^30.0.5", "jest-sonar-reporter": "^2.0.0", + "jiti": "^2.5.1", "prettier": "^3.6.2", + "supertest": "^7.1.4", "ts-jest": "^29.4.1", "tsx": "^4.16.2", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "typescript-eslint": "^8.42.0" } } diff --git a/src/controllers/tests/orders-controller.test.ts b/src/controllers/tests/orders-controller.test.ts index dfa9bcd..f5a72df 100644 --- a/src/controllers/tests/orders-controller.test.ts +++ b/src/controllers/tests/orders-controller.test.ts @@ -1,77 +1,579 @@ import { Request, Response, NextFunction } from 'express'; -import { OrdersController } from '@/controllers/orders-controller'; +import { OrdersController } from '../orders-controller'; +import { knex } from '@/database/knex'; import { AppError } from '@/utils/AppError'; -const knexMock: Record = { - where: jest.fn().mockReturnThis(), - first: jest.fn(), - select: jest.fn().mockReturnThis(), - join: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - insert: jest.fn(), - raw: jest.fn(), -}; - -jest.mock('@/database/knex', () => { - return { - knex: jest.fn(() => knexMock), - }; -}); +// Mock das dependências +jest.mock('@/database/knex'); +jest.mock('@/utils/AppError'); + +const mockKnex = knex as jest.Mocked; -describe('OrdersController - create', () => { - let ordersController: OrdersController; +describe('OrdersController', () => { + let controller: OrdersController; + let mockRequest: Partial; let mockResponse: Partial; - let mockNext: jest.Mock; + let mockNext: NextFunction; beforeEach(() => { - ordersController = new OrdersController(); - + controller = new OrdersController(); + mockRequest = {}; mockResponse = { status: jest.fn().mockReturnThis(), - json: jest.fn(), + json: jest.fn().mockReturnThis(), }; - mockNext = jest.fn(); + + // Reset todos os mocks jest.clearAllMocks(); }); - it('should throw error if session not exists', async () => { - knexMock.first.mockResolvedValueOnce(undefined); - const mockRequest = { - body: { table_session_id: 1, product_id: 1, quantity: 2 }, - } as Request; - - await ordersController.create( - mockRequest, - mockResponse as Response, - mockNext as NextFunction, - ); - - expect(mockNext).toHaveBeenCalledWith(expect.any(AppError)); - expect((mockNext.mock.calls[0][0] as AppError).message).toBe( - 'session table not found', - ); + describe('create', () => { + it('should create a new order successfully', async () => { + // Arrange + mockRequest.body = { + table_session_id: 1, + product_id: 2, + quantity: 3, + }; + + const mockSession = { + id: 1, + table_id: 5, + opened_at: new Date(), + closed_at: null, + }; + + const mockProduct = { + id: 2, + name: 'Pizza', + price: 25.5, + }; + + const mockSessionChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(mockSession), + }; + + const mockProductChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(mockProduct), + }; + + const mockInsertChain = { + insert: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any) + .mockReturnValueOnce(mockSessionChain) // tables_sessions query + .mockReturnValueOnce(mockProductChain) // products query + .mockReturnValueOnce(mockInsertChain); // orders insert + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockSessionChain.where).toHaveBeenCalledWith({ id: 1 }); + expect(mockProductChain.where).toHaveBeenCalledWith({ id: 2 }); + expect(mockInsertChain.insert).toHaveBeenCalledWith({ + table_session_id: 1, + product_id: 2, + quantity: 3, + price: 25.5, + }); + expect(mockResponse.status).toHaveBeenCalledWith(201); + expect(mockResponse.json).toHaveBeenCalled(); + }); + + it('should throw error when session not found', async () => { + // Arrange + mockRequest.body = { + table_session_id: 999, + product_id: 2, + quantity: 3, + }; + + const mockSessionChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(null), + }; + + (mockKnex as any).mockReturnValueOnce(mockSessionChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(AppError).toHaveBeenCalledWith('session table not found'); + }); + + it('should throw error when session is closed', async () => { + // Arrange + mockRequest.body = { + table_session_id: 1, + product_id: 2, + quantity: 3, + }; + + const mockClosedSession = { + id: 1, + table_id: 5, + opened_at: new Date(), + closed_at: new Date(), + }; + + const mockSessionChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(mockClosedSession), + }; + + (mockKnex as any).mockReturnValueOnce(mockSessionChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(AppError).toHaveBeenCalledWith('this table is closed'); + }); + + it('should throw error when product not found', async () => { + // Arrange + mockRequest.body = { + table_session_id: 1, + product_id: 999, + quantity: 3, + }; + + const mockSession = { + id: 1, + table_id: 5, + opened_at: new Date(), + closed_at: null, + }; + + const mockSessionChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(mockSession), + }; + + const mockProductChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(null), + }; + + (mockKnex as any) + .mockReturnValueOnce(mockSessionChain) + .mockReturnValueOnce(mockProductChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(AppError).toHaveBeenCalledWith('product not found'); + }); + + it('should throw validation error for invalid body data', async () => { + // Arrange + mockRequest.body = { + table_session_id: 'invalid', + product_id: 2, + quantity: 3, + }; + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + + it('should throw validation error for missing fields', async () => { + // Arrange + mockRequest.body = { + table_session_id: 1, + // product_id missing + quantity: 3, + }; + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + + it('should use product price when creating order', async () => { + // Arrange + mockRequest.body = { + table_session_id: 1, + product_id: 2, + quantity: 2, + }; + + const mockSession = { + id: 1, + closed_at: null, + }; + + const mockProduct = { + id: 2, + name: 'Burger', + price: 15.99, + }; + + const mockSessionChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(mockSession), + }; + + const mockProductChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(mockProduct), + }; + + const mockInsertChain = { + insert: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any) + .mockReturnValueOnce(mockSessionChain) + .mockReturnValueOnce(mockProductChain) + .mockReturnValueOnce(mockInsertChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockInsertChain.insert).toHaveBeenCalledWith({ + table_session_id: 1, + product_id: 2, + quantity: 2, + price: 15.99, + }); + }); + }); + + describe('index', () => { + it('should return orders for a table session with join', async () => { + // Arrange + mockRequest.params = { table_session_id: '1' }; + + const mockOrders = [ + { + id: 1, + table_session_id: 1, + product_id: 2, + name: 'Pizza', + price: 25.5, + quantity: 2, + total: 51.0, + created_at: new Date(), + updated_at: new Date(), + }, + { + id: 2, + table_session_id: 1, + product_id: 3, + name: 'Burger', + price: 15.99, + quantity: 1, + total: 15.99, + created_at: new Date(), + updated_at: new Date(), + }, + ]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + join: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue(mockOrders), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + mockKnex.raw = jest + .fn() + .mockReturnValue('(orders.price * orders.quantity) AS total') as any; + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnex).toHaveBeenCalledWith('orders'); + expect(mockKnexChain.select).toHaveBeenCalledWith( + 'orders.id', + 'orders.table_session_id', + 'orders.product_id', + 'products.name', + 'orders.price', + 'orders.quantity', + '(orders.price * orders.quantity) AS total', + 'orders.created_at', + 'orders.updated_at', + ); + expect(mockKnexChain.join).toHaveBeenCalledWith( + 'products', + 'products.id', + 'orders.product_id', + ); + expect(mockKnexChain.where).toHaveBeenCalledWith({ table_session_id: 1 }); + expect(mockKnexChain.orderBy).toHaveBeenCalledWith( + 'orders.created_at', + 'desc', + ); + expect(mockResponse.json).toHaveBeenCalledWith(mockOrders); + }); + + it('should throw validation error for invalid table_session_id', async () => { + // Arrange + mockRequest.params = { table_session_id: 'invalid' }; + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + + it('should return empty array when no orders found', async () => { + // Arrange + mockRequest.params = { table_session_id: '999' }; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + join: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue([]), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + mockKnex.raw = jest + .fn() + .mockReturnValue('(orders.price * orders.quantity) AS total') as any; + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockResponse.json).toHaveBeenCalledWith([]); + }); + + it('should call next with error when database operation fails', async () => { + // Arrange + mockRequest.params = { table_session_id: '1' }; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + join: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockRejectedValue(new Error('Database error')), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + mockKnex.raw = jest + .fn() + .mockReturnValue('(orders.price * orders.quantity) AS total') as any; + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); + }); }); - it('should create order', async () => { - knexMock.first - .mockResolvedValueOnce({ id: 1, closed_at: null }) - .mockResolvedValueOnce({ id: 1, price: 10 }); + describe('show', () => { + it('should return order summary with total and quantity', async () => { + // Arrange + mockRequest.params = { table_session_id: '1' }; + + const mockSummary = [ + { + total: 66.99, + quantity: 3, + }, + ]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockResolvedValue(mockSummary), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + mockKnex.raw = jest + .fn() + .mockReturnValueOnce( + 'COALESCE(SUM(orders.price * orders.quantity), 0) AS total', + ) + .mockReturnValueOnce( + 'COALESCE(SUM(orders.quantity), 0) AS quantity', + ) as any; + + // Act + await controller.show( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnex).toHaveBeenCalledWith('orders'); + expect(mockKnexChain.select).toHaveBeenCalledWith( + 'COALESCE(SUM(orders.price * orders.quantity), 0) AS total', + 'COALESCE(SUM(orders.quantity), 0) AS quantity', + ); + expect(mockKnexChain.where).toHaveBeenCalledWith({ + table_session_id: '1', + }); + expect(mockResponse.json).toHaveBeenCalledWith(mockSummary); + }); + + it('should return zero values when no orders exist', async () => { + // Arrange + mockRequest.params = { table_session_id: '999' }; + + const mockEmptySummary = [ + { + total: 0, + quantity: 0, + }, + ]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockResolvedValue(mockEmptySummary), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + mockKnex.raw = jest + .fn() + .mockReturnValueOnce( + 'COALESCE(SUM(orders.price * orders.quantity), 0) AS total', + ) + .mockReturnValueOnce( + 'COALESCE(SUM(orders.quantity), 0) AS quantity', + ) as any; + + // Act + await controller.show( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockResponse.json).toHaveBeenCalledWith(mockEmptySummary); + }); + + it('should handle string table_session_id parameter', async () => { + // Arrange + mockRequest.params = { table_session_id: '5' }; + + const mockSummary = [{ total: 50.0, quantity: 2 }]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockResolvedValue(mockSummary), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + mockKnex.raw = jest + .fn() + .mockReturnValueOnce( + 'COALESCE(SUM(orders.price * orders.quantity), 0) AS total', + ) + .mockReturnValueOnce( + 'COALESCE(SUM(orders.quantity), 0) AS quantity', + ) as any; + + // Act + await controller.show( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnexChain.where).toHaveBeenCalledWith({ + table_session_id: '5', + }); + }); + + it('should call next with error when database operation fails', async () => { + // Arrange + mockRequest.params = { table_session_id: '1' }; - knexMock.insert.mockResolvedValueOnce([1]); + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockRejectedValue(new Error('Database error')), + }; - const mockRequest = { - body: { table_session_id: 1, product_id: 1, quantity: 2 }, - } as Request; + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + mockKnex.raw = jest + .fn() + .mockReturnValueOnce( + 'COALESCE(SUM(orders.price * orders.quantity), 0) AS total', + ) + .mockReturnValueOnce( + 'COALESCE(SUM(orders.quantity), 0) AS quantity', + ) as any; - await ordersController.create( - mockRequest, - mockResponse as Response, - mockNext as NextFunction, - ); + // Act + await controller.show( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); - expect(mockResponse.status).toHaveBeenCalledWith(201); - expect(mockResponse.json).toHaveBeenCalledWith(); - expect(mockNext).not.toHaveBeenCalled(); + // Assert + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); + }); }); }); diff --git a/src/controllers/tests/products-controller.test.ts b/src/controllers/tests/products-controller.test.ts new file mode 100644 index 0000000..4c4afa2 --- /dev/null +++ b/src/controllers/tests/products-controller.test.ts @@ -0,0 +1,473 @@ +import { Request, Response, NextFunction } from 'express'; +import { ProductController } from '../products-controllers'; +import { knex } from '@/database/knex'; +import { AppError } from '@/utils/AppError'; + +// Mock das dependências +jest.mock('@/database/knex'); +jest.mock('@/utils/AppError'); + +const mockKnex = knex as jest.Mocked; + +describe('ProductController', () => { + let controller: ProductController; + let mockRequest: Partial; + let mockResponse: Partial; + let mockNext: NextFunction; + + beforeEach(() => { + controller = new ProductController(); + mockRequest = {}; + mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + mockNext = jest.fn(); + + // Reset todos os mocks + jest.clearAllMocks(); + }); + + describe('index', () => { + it('should return all products when no name filter is provided', async () => { + // Arrange + mockRequest.query = {}; + const mockProducts = [ + { id: 1, name: 'Product 1', price: 10.5 }, + { id: 2, name: 'Product 2', price: 20.0 }, + ]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + whereLike: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue(mockProducts), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnex).toHaveBeenCalledWith('products'); + expect(mockKnexChain.select).toHaveBeenCalled(); + expect(mockKnexChain.whereLike).toHaveBeenCalledWith('name', '%%'); + expect(mockKnexChain.orderBy).toHaveBeenCalledWith('name'); + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith(mockProducts); + }); + + it('should filter products by name when name query is provided', async () => { + // Arrange + mockRequest.query = { name: 'Pizza' }; + const mockProducts = [ + { id: 1, name: 'Pizza Margherita', price: 25.0 }, + { id: 2, name: 'Pizza Pepperoni', price: 28.0 }, + ]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + whereLike: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue(mockProducts), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnexChain.whereLike).toHaveBeenCalledWith('name', '%Pizza%'); + expect(mockResponse.json).toHaveBeenCalledWith(mockProducts); + }); + + it('should call next with error when database operation fails', async () => { + // Arrange + mockRequest.query = {}; + const databaseError = new Error('Database error'); + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + whereLike: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockRejectedValue(databaseError), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(databaseError); + expect(mockResponse.json).not.toHaveBeenCalled(); + }); + }); + + describe('create', () => { + it('should create a new product successfully', async () => { + // Arrange + mockRequest.body = { + name: 'New Product', + price: 15.5, + }; + + const mockInsertChain = { + insert: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any).mockReturnValueOnce(mockInsertChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnex).toHaveBeenCalledWith('products'); + expect(mockInsertChain.insert).toHaveBeenCalledWith({ + name: 'New Product', + price: 15.5, + }); + expect(mockResponse.status).toHaveBeenCalledWith(201); + expect(mockResponse.json).toHaveBeenCalled(); + }); + + it('should throw validation error for invalid name (too short)', async () => { + // Arrange + mockRequest.body = { + name: 'abc', + price: 15.5, + }; + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + expect(mockResponse.status).not.toHaveBeenCalled(); + }); + + it('should throw validation error for invalid price (zero or negative)', async () => { + // Arrange + mockRequest.body = { + name: 'Valid Product Name', + price: 0, + }; + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + expect(mockResponse.status).not.toHaveBeenCalled(); + }); + + it('should throw validation error for missing fields', async () => { + // Arrange + mockRequest.body = { + name: 'Valid Product Name', + // price missing + }; + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + + it('should trim product name before saving', async () => { + // Arrange + mockRequest.body = { + name: ' Trimmed Product ', + price: 15.5, + }; + + const mockInsertChain = { + insert: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any).mockReturnValueOnce(mockInsertChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockInsertChain.insert).toHaveBeenCalledWith({ + name: 'Trimmed Product', + price: 15.5, + }); + }); + + it('should call next with error when database insert fails', async () => { + // Arrange + mockRequest.body = { + name: 'Valid Product', + price: 15.5, + }; + + const mockInsertChain = { + insert: jest.fn().mockRejectedValue(new Error('Insert failed')), + }; + + (mockKnex as any).mockReturnValueOnce(mockInsertChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); + }); + }); + + describe('update', () => { + it('should update a product successfully', async () => { + // Arrange + mockRequest.params = { id: '1' }; + mockRequest.body = { + name: 'Updated Product', + price: 25.0, + }; + + const existingProduct = { id: 1, name: 'Old Product', price: 20.0 }; + + const mockFindChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(existingProduct), + }; + + const mockUpdateChain = { + update: jest.fn().mockReturnThis(), + where: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any).mockReturnValueOnce(mockFindChain); + (mockKnex as any).mockReturnValueOnce(mockUpdateChain); + mockKnex.fn = { + now: jest.fn().mockReturnValue('CURRENT_TIMESTAMP'), + } as any; + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockFindChain.where).toHaveBeenCalledWith({ id: 1 }); + expect(mockUpdateChain.update).toHaveBeenCalledWith({ + name: 'Updated Product', + price: 25.0, + updated_at: 'CURRENT_TIMESTAMP', + }); + expect(mockUpdateChain.where).toHaveBeenCalledWith({ id: 1 }); + expect(mockResponse.json).toHaveBeenCalled(); + }); + + it('should throw error when product not found', async () => { + // Arrange + mockRequest.params = { id: '999' }; + mockRequest.body = { + name: 'Updated Product', + price: 25.0, + }; + + const mockFindChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(null), + }; + + (mockKnex as any).mockReturnValueOnce(mockFindChain); + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(AppError).toHaveBeenCalledWith('Product not found'); + }); + + it('should throw validation error for invalid id parameter', async () => { + // Arrange + mockRequest.params = { id: 'invalid' }; + mockRequest.body = { + name: 'Updated Product', + price: 25.0, + }; + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + + it('should throw validation error for invalid body data', async () => { + // Arrange + mockRequest.params = { id: '1' }; + mockRequest.body = { + name: 'abc', // too short + price: 25.0, + }; + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + }); + + describe('remove', () => { + it('should delete a product successfully', async () => { + // Arrange + mockRequest.params = { id: '1' }; + + const existingProduct = { id: 1, name: 'Product to Delete', price: 20.0 }; + + const mockFindChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(existingProduct), + }; + + const mockDeleteChain = { + delete: jest.fn().mockReturnThis(), + where: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any).mockReturnValueOnce(mockFindChain); + (mockKnex as any).mockReturnValueOnce(mockDeleteChain); + + // Act + await controller.remove( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockFindChain.where).toHaveBeenCalledWith({ id: 1 }); + expect(mockDeleteChain.delete).toHaveBeenCalled(); + expect(mockDeleteChain.where).toHaveBeenCalledWith({ id: 1 }); + expect(mockResponse.json).toHaveBeenCalled(); + }); + + it('should throw error when product not found for deletion', async () => { + // Arrange + mockRequest.params = { id: '999' }; + + const mockFindChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(null), + }; + + (mockKnex as any).mockReturnValueOnce(mockFindChain); + + // Act + await controller.remove( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(AppError).toHaveBeenCalledWith('Product not found'); + }); + + it('should throw validation error for invalid id parameter', async () => { + // Arrange + mockRequest.params = { id: 'invalid' }; + + // Act + await controller.remove( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + + it('should call next with error when database delete fails', async () => { + // Arrange + mockRequest.params = { id: '1' }; + + const existingProduct = { id: 1, name: 'Product', price: 20.0 }; + + const mockFindChain = { + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(existingProduct), + }; + + const mockDeleteChain = { + delete: jest.fn().mockReturnThis(), + where: jest.fn().mockRejectedValue(new Error('Delete failed')), + }; + + (mockKnex as any).mockReturnValueOnce(mockFindChain); + (mockKnex as any).mockReturnValueOnce(mockDeleteChain); + + // Act + await controller.remove( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); + }); + }); +}); diff --git a/src/controllers/tests/tables-controller.test.ts b/src/controllers/tests/tables-controller.test.ts new file mode 100644 index 0000000..afca066 --- /dev/null +++ b/src/controllers/tests/tables-controller.test.ts @@ -0,0 +1,217 @@ +import { Request, Response, NextFunction } from 'express'; +import { TablesController } from '../tables-controller'; +import { knex } from '@/database/knex'; + +// Mock das dependências +jest.mock('@/database/knex'); + +const mockKnex = knex as jest.Mocked; + +describe('TablesController', () => { + let controller: TablesController; + let mockRequest: Partial; + let mockResponse: Partial; + let mockNext: NextFunction; + + beforeEach(() => { + controller = new TablesController(); + mockRequest = {}; + mockResponse = { + json: jest.fn().mockReturnThis(), + }; + mockNext = jest.fn(); + + // Reset todos os mocks + jest.clearAllMocks(); + }); + + describe('index', () => { + it('should return all tables ordered by table_number', async () => { + // Arrange + const mockTables = [ + { id: 1, table_number: 1, status: 'available' }, + { id: 2, table_number: 2, status: 'occupied' }, + { id: 3, table_number: 3, status: 'reserved' }, + ]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue(mockTables), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnex).toHaveBeenCalledWith('tables'); + expect(mockKnexChain.select).toHaveBeenCalled(); + expect(mockKnexChain.orderBy).toHaveBeenCalledWith('table_number'); + expect(mockResponse.json).toHaveBeenCalledWith(mockTables); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should return empty array when no tables exist', async () => { + // Arrange + const mockTables: any[] = []; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue(mockTables), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnex).toHaveBeenCalledWith('tables'); + expect(mockKnexChain.select).toHaveBeenCalled(); + expect(mockKnexChain.orderBy).toHaveBeenCalledWith('table_number'); + expect(mockResponse.json).toHaveBeenCalledWith(mockTables); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should call next with error when database operation fails', async () => { + // Arrange + const databaseError = new Error('Database connection failed'); + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockRejectedValue(databaseError), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnex).toHaveBeenCalledWith('tables'); + expect(mockKnexChain.select).toHaveBeenCalled(); + expect(mockKnexChain.orderBy).toHaveBeenCalledWith('table_number'); + expect(mockResponse.json).not.toHaveBeenCalled(); + expect(mockNext).toHaveBeenCalledWith(databaseError); + }); + + it('should handle SQL errors appropriately', async () => { + // Arrange + const sqlError = new Error('Table "tables" does not exist'); + sqlError.name = 'SqlError'; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockRejectedValue(sqlError), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(sqlError); + expect(mockResponse.json).not.toHaveBeenCalled(); + }); + + it('should handle network errors appropriately', async () => { + // Arrange + const networkError = new Error('Connection timeout'); + networkError.name = 'NetworkError'; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockRejectedValue(networkError), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(networkError); + expect(mockResponse.json).not.toHaveBeenCalled(); + }); + + it('should verify the correct knex query chain is called', async () => { + // Arrange + const mockTables = [ + { id: 1, table_number: 5 }, + { id: 2, table_number: 10 }, + { id: 3, table_number: 15 }, + ]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue(mockTables), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + // Verifica se a sequência de chamadas está correta + expect(mockKnex).toHaveBeenCalledTimes(1); + expect(mockKnexChain.select).toHaveBeenCalledTimes(1); + expect(mockKnexChain.orderBy).toHaveBeenCalledTimes(1); + expect(mockKnexChain.orderBy).toHaveBeenCalledWith('table_number'); + + // Verifica se o resultado foi retornado corretamente + expect(mockResponse.json).toHaveBeenCalledWith(mockTables); + }); + + it('should not modify request or response when successful', async () => { + // Arrange + const originalRequest = { ...mockRequest }; + const mockTables = [{ id: 1, table_number: 1 }]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue(mockTables), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockRequest).toEqual(originalRequest); + expect(mockResponse.json).toHaveBeenCalledTimes(1); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/controllers/tests/tables-sessions-controller.test.ts b/src/controllers/tests/tables-sessions-controller.test.ts new file mode 100644 index 0000000..121e140 --- /dev/null +++ b/src/controllers/tests/tables-sessions-controller.test.ts @@ -0,0 +1,365 @@ +import { Request, Response, NextFunction } from 'express'; +import { TablesSessionsController } from '../tables-sessions-controller'; +import { knex } from '@/database/knex'; +import { AppError } from '@/utils/AppError'; + +// Mock das dependências +jest.mock('@/database/knex'); +jest.mock('@/utils/AppError'); + +const mockKnex = knex as jest.Mocked; + +describe('TablesSessionsController', () => { + let controller: TablesSessionsController; + let mockRequest: Partial; + let mockResponse: Partial; + let mockNext: NextFunction; + + beforeEach(() => { + controller = new TablesSessionsController(); + mockRequest = {}; + mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + mockNext = jest.fn(); + + // Reset todos os mocks + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should create a new table session successfully', async () => { + // Arrange + mockRequest.body = { table_id: 1 }; + + const mockKnexChain = { + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(null), // Nenhuma sessão encontrada + }; + + const mockInsertChain = { + insert: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + (mockKnex as any).mockReturnValueOnce(mockInsertChain); + mockKnex.fn = { + now: jest.fn().mockReturnValue('CURRENT_TIMESTAMP'), + } as any; + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnexChain.where).toHaveBeenCalledWith({ table_id: 1 }); + expect(mockKnexChain.orderBy).toHaveBeenCalledWith('opened_at', 'desc'); + expect(mockKnexChain.first).toHaveBeenCalled(); + expect(mockInsertChain.insert).toHaveBeenCalledWith({ + table_id: 1, + opened_at: 'CURRENT_TIMESTAMP', + }); + expect(mockResponse.status).toHaveBeenCalledWith(201); + expect(mockResponse.json).toHaveBeenCalled(); + }); + + it('should create a new session when existing session is closed', async () => { + // Arrange + mockRequest.body = { table_id: 1 }; + + const existingClosedSession = { + id: 1, + table_id: 1, + opened_at: new Date(), + closed_at: new Date(), + }; + + const mockKnexChain = { + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(existingClosedSession), + }; + + const mockInsertChain = { + insert: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + (mockKnex as any).mockReturnValueOnce(mockInsertChain); + mockKnex.fn = { + now: jest.fn().mockReturnValue('CURRENT_TIMESTAMP'), + } as any; + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockResponse.status).toHaveBeenCalledWith(201); + expect(mockResponse.json).toHaveBeenCalled(); + }); + + it('should throw error when table already has an open session', async () => { + // Arrange + mockRequest.body = { table_id: 1 }; + + const existingOpenSession = { + id: 1, + table_id: 1, + opened_at: new Date(), + closed_at: null, + }; + + const mockKnexChain = { + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(existingOpenSession), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(AppError).toHaveBeenCalledWith( + 'this table has already been opened', + ); + }); + + it('should throw error for invalid table_id', async () => { + // Arrange + mockRequest.body = { table_id: 'invalid' }; + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + + it('should call next with error when database operation fails', async () => { + // Arrange + mockRequest.body = { table_id: 1 }; + + const mockKnexChain = { + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + first: jest.fn().mockRejectedValue(new Error('Database error')), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.create( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); + }); + }); + + describe('index', () => { + it('should return all sessions ordered by closed_at', async () => { + // Arrange + const mockSessions = [ + { id: 1, table_id: 1, opened_at: new Date(), closed_at: null }, + { id: 2, table_id: 2, opened_at: new Date(), closed_at: new Date() }, + ]; + + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockResolvedValue(mockSessions), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockKnexChain.select).toHaveBeenCalled(); + expect(mockKnexChain.orderBy).toHaveBeenCalledWith('closed_at'); + expect(mockResponse.json).toHaveBeenCalledWith(mockSessions); + }); + + it('should call next with error when database operation fails', async () => { + // Arrange + const mockKnexChain = { + select: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockRejectedValue(new Error('Database error')), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.index( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); + }); + }); + + describe('update', () => { + it('should close a session successfully', async () => { + // Arrange + mockRequest.params = { id: '1' }; + + const existingSession = { + id: 1, + table_id: 1, + opened_at: new Date(), + closed_at: null, + }; + + const mockFindChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(existingSession), + }; + + const mockUpdateChain = { + update: jest.fn().mockReturnThis(), + where: jest.fn().mockResolvedValue([1]), + }; + + (mockKnex as any).mockReturnValueOnce(mockFindChain); + (mockKnex as any).mockReturnValueOnce(mockUpdateChain); + mockKnex.fn = { + now: jest.fn().mockReturnValue('CURRENT_TIMESTAMP'), + } as any; + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockFindChain.where).toHaveBeenCalledWith({ id: 1 }); + expect(mockFindChain.first).toHaveBeenCalled(); + expect(mockUpdateChain.update).toHaveBeenCalledWith({ + closed_at: 'CURRENT_TIMESTAMP', + }); + expect(mockUpdateChain.where).toHaveBeenCalledWith({ id: 1 }); + expect(mockResponse.json).toHaveBeenCalled(); + }); + + it('should throw error when session not found', async () => { + // Arrange + mockRequest.params = { id: '1' }; + + const mockKnexChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(null), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(AppError).toHaveBeenCalledWith('session table not found'); + }); + + it('should throw error when session is already closed', async () => { + // Arrange + mockRequest.params = { id: '1' }; + + const existingClosedSession = { + id: 1, + table_id: 1, + opened_at: new Date(), + closed_at: new Date(), + }; + + const mockKnexChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockResolvedValue(existingClosedSession), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(AppError).toHaveBeenCalledWith( + 'this session table has already been closed', + ); + }); + + it('should throw error for invalid id parameter', async () => { + // Arrange + mockRequest.params = { id: 'invalid' }; + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalled(); + }); + + it('should call next with error when database operation fails', async () => { + // Arrange + mockRequest.params = { id: '1' }; + + const mockKnexChain = { + where: jest.fn().mockReturnThis(), + first: jest.fn().mockRejectedValue(new Error('Database error')), + }; + + (mockKnex as any).mockReturnValueOnce(mockKnexChain); + + // Act + await controller.update( + mockRequest as Request, + mockResponse as Response, + mockNext, + ); + + // Assert + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); + }); + }); +}); diff --git a/src/routes/tests/health.test.ts b/src/routes/tests/health.test.ts new file mode 100644 index 0000000..6dcacbd --- /dev/null +++ b/src/routes/tests/health.test.ts @@ -0,0 +1,187 @@ +import request from 'supertest'; +import express from 'express'; +import { healthRoutes } from '../health'; + +// Alternativa: Teste unitário direto do handler +describe('Health Routes - Unit Tests', () => { + let mockRequest: any; + let mockResponse: any; + let jsonSpy: jest.Mock; + let statusSpy: jest.Mock; + let sendSpy: jest.Mock; + + beforeEach(() => { + mockRequest = {}; + + jsonSpy = jest.fn(); + statusSpy = jest.fn().mockReturnThis(); + sendSpy = jest.fn(); + + mockResponse = { + json: jsonSpy, + status: statusSpy, + send: sendSpy, + }; + + jest.clearAllMocks(); + }); + + // Extraindo o handler da rota para teste direto + const healthHandler = async (_: any, res: any) => { + try { + res.json({ status: 'OK', timeStamp: new Date().toISOString() }); + } catch (error) { + res.status(500).send('Error generating metrics'); + } + }; + + describe('Health Handler', () => { + it('should return status OK with timestamp', async () => { + // Arrange + const mockDate = new Date('2023-10-15T10:30:00.000Z'); + const originalDate = global.Date; + global.Date = jest.fn(() => mockDate) as any; + // @ts-ignore + global.Date.prototype = originalDate.prototype; + + // Act + await healthHandler(mockRequest, mockResponse); + + // Assert + expect(jsonSpy).toHaveBeenCalledWith({ + status: 'OK', + timeStamp: '2023-10-15T10:30:00.000Z', + }); + + // Restore + global.Date = originalDate; + }); + + it('should return current timestamp when called', async () => { + // Act + await healthHandler(mockRequest, mockResponse); + + // Assert + expect(jsonSpy).toHaveBeenCalledTimes(1); + const calledWith = jsonSpy.mock.calls[0][0]; + + expect(calledWith).toHaveProperty('status', 'OK'); + expect(calledWith).toHaveProperty('timeStamp'); + expect(typeof calledWith.timeStamp).toBe('string'); + expect(calledWith.timeStamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, + ); + }); + + it('should handle Date constructor errors', async () => { + // Arrange + const originalDate = global.Date; + global.Date = jest.fn(() => { + throw new Error('Date constructor error'); + }) as any; + + // Act + await healthHandler(mockRequest, mockResponse); + + // Assert + expect(statusSpy).toHaveBeenCalledWith(500); + expect(sendSpy).toHaveBeenCalledWith('Error generating metrics'); + expect(jsonSpy).not.toHaveBeenCalled(); + + // Restore + global.Date = originalDate; + }); + + it('should handle res.json errors', async () => { + // Arrange + jsonSpy.mockImplementation(() => { + throw new Error('JSON error'); + }); + + // Act + await healthHandler(mockRequest, mockResponse); + + // Assert + expect(jsonSpy).toHaveBeenCalled(); + expect(statusSpy).toHaveBeenCalledWith(500); + expect(sendSpy).toHaveBeenCalledWith('Error generating metrics'); + }); + + it('should not call error methods on success', async () => { + // Act + await healthHandler(mockRequest, mockResponse); + + // Assert + expect(statusSpy).not.toHaveBeenCalled(); + expect(sendSpy).not.toHaveBeenCalled(); + expect(jsonSpy).toHaveBeenCalledTimes(1); + }); + }); +}); + +// Teste de integração usando supertest +describe('Health Routes - Integration Tests', () => { + let app: express.Application; + + beforeAll(() => { + app = express(); + app.use('/health', healthRoutes); + }); + + describe('GET /health', () => { + it('should return 200 with status OK and timestamp', async () => { + // Act + const response = await request(app).get('/health').expect(200); + + // Assert + expect(response.body).toHaveProperty('status', 'OK'); + expect(response.body).toHaveProperty('timeStamp'); + expect(typeof response.body.timeStamp).toBe('string'); + expect(response.body.timeStamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, + ); + }); + + it('should return valid timestamp format', async () => { + // Act + const response = await request(app).get('/health').expect(200); + + // Assert + const timestamp = response.body.timeStamp; + expect(() => new Date(timestamp)).not.toThrow(); + expect(new Date(timestamp).toISOString()).toBe(timestamp); + }); + + it('should return different timestamps on consecutive calls', async () => { + // Act + const response1 = await request(app).get('/health'); + + // Small delay to ensure different timestamps + await new Promise((resolve) => setTimeout(resolve, 1)); + + const response2 = await request(app).get('/health'); + + // Assert + expect(response1.body.timeStamp).not.toBe(response2.body.timeStamp); + expect(response1.body.status).toBe('OK'); + expect(response2.body.status).toBe('OK'); + }); + + it('should have correct response headers', async () => { + // Act + const response = await request(app).get('/health').expect(200); + + // Assert + expect(response.headers['content-type']).toMatch(/application\/json/); + }); + }); +}); + +// Teste para verificar se a rota está configurada corretamente +describe('Health Routes - Route Configuration', () => { + it('should have the correct route defined', () => { + // Verificar se a rota está configurada + expect(healthRoutes).toBeDefined(); + expect(typeof healthRoutes).toBe('function'); + }); +});