diff --git a/jest.config.ts b/jest.config.ts index 9ce5f9b..7aeba40 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -6,22 +6,53 @@ const createJestConfig = nextJest({ }); const config: Config = { - coverageProvider: "v8", - testEnvironment: "jsdom", - clearMocks: true, + verbose: true, + projects: [ + { + displayName: "client", + clearMocks: true, + testEnvironment: "jsdom", + testMatch: [ + "**/tests/unit/**/*.+(test|spec).[jt]s?(x)", + "**/tests/integration/**/*.client.+(test|spec).[jt]s?(x)", + "**/*.client.+(test|spec).[jt]s?(x)", + ], + transform: { + "^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }], + }, + moduleNameMapper: { + "^@/(.*)$": "/$1", + }, + setupFilesAfterEnv: ["/jest.setup.ts"], + testPathIgnorePatterns: [".*\\.server\\.(test|spec)\\.[jt]s?(x)$"], + }, + { + displayName: "server", + clearMocks: true, + testEnvironment: "node", + testMatch: ["**/tests/integration/**/*.server.+(test|spec).[jt]s?(x)", "**/*.server.+(test|spec).[jt]s?(x)"], + testPathIgnorePatterns: [".*\\.client\\.(test|spec)\\.[jt]s?(x)$"], + moduleNameMapper: { + "^@/(.*)$": "/$1", + }, + setupFilesAfterEnv: ["/jest.server.setup.ts"], + transform: { + "^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }], + }, + }, + ], collectCoverage: true, coverageDirectory: "coverage", + coverageReporters: ["html", ["text", { skipFull: true }], "text-summary"], collectCoverageFrom: [ "components/**/*.{js,jsx,ts,tsx}", "lib/**/*.{js,ts}", + "app/**/*.{js,jsx,ts,tsx}", "!**/*.d.ts", "!**/node_modules/**", "!**/*.test.{js,jsx,ts,tsx}", + "!**/*.spec.{js,jsx,ts,tsx}", ], - moduleNameMapper: { - "^@/(.*)$": "/$1", - }, - setupFilesAfterEnv: ["/jest.setup.ts"], }; export default createJestConfig(config); diff --git a/jest.server.setup.ts b/jest.server.setup.ts new file mode 100644 index 0000000..7f05a12 --- /dev/null +++ b/jest.server.setup.ts @@ -0,0 +1,40 @@ +import "@testing-library/jest-dom"; +import * as integrationDb from "./tests/config/db-integration"; +import { mockAuth } from "./tests/mocks"; + +jest.mock("./auth", () => ({ + auth: mockAuth, +})); + +jest.mock("@/lib/mongoose", () => ({ + __esModule: true, + default: jest.fn(() => Promise.resolve()), +})); + +beforeAll(async () => { + await integrationDb.connectDB(); +}, 30000); + +beforeEach(async () => { + if (integrationDb.isDBConnected()) { + await integrationDb.clearDB(); + } +}, 10000); + +afterAll(async () => { + await integrationDb.clearDB(); +}); + +afterAll(async () => { + await integrationDb.disconnectDB(); +}, 10000); + +process.on("SIGINT", async () => { + await integrationDb.disconnectDB(); + process.exit(0); +}); + +process.on("SIGTERM", async () => { + await integrationDb.disconnectDB(); + process.exit(0); +}); diff --git a/package-lock.json b/package-lock.json index cbd205f..3e57601 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "eslint-plugin-prettier": "^5.5.4", "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", + "mongodb-memory-server": "^11.0.1", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4", @@ -234,6 +235,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1046,6 +1048,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -1134,6 +1137,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -1143,6 +1147,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.5.tgz", "integrity": "sha512-SFVsNAgsAoou+BjRewMqN+m9jaztB9wCWN9RSRgePqUbq8UVlvJfku5zB2KVhLPgH/h0RLk38tvd4tGeAhygnw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1317,6 +1322,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1340,6 +1346,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2968,6 +2975,7 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } @@ -4900,6 +4908,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5243,6 +5252,7 @@ "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -5252,6 +5262,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5262,6 +5273,7 @@ "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5364,6 +5376,7 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -5895,6 +5908,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6258,6 +6272,16 @@ "node": ">= 0.4" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -6419,6 +6443,104 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.6.0.tgz", + "integrity": "sha512-2YkS7NuiJceSEbyEOdSNLE9tsGd+f4+f7C+Nik/MCk27SYdwIMPT/yRKvg++FZhQXgk0KWJKJyXX9RhVV0RGqA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz", + "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.12.0.tgz", + "integrity": "sha512-w28i8lkBgREV3rPXGbgK+BO66q+ZpKqRWrZLiCdmmUlLPrQ45CzkvRhN+7lnv00Gpi2zy5naRxnUFAxCECDm9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6518,6 +6640,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -6575,6 +6698,16 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6992,6 +7125,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/compute-scroll-into-view": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz", @@ -7781,6 +7921,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7870,6 +8011,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7971,6 +8113,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8378,6 +8521,16 @@ "es5-ext": "~0.10.14" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource-parser": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", @@ -8481,6 +8634,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -8602,6 +8762,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8640,6 +8844,27 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -9889,7 +10114,6 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "license": "MIT", - "peer": true, "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" @@ -10995,6 +11219,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -11188,7 +11413,6 @@ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "license": "MIT", - "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -12748,6 +12972,139 @@ "whatwg-url": "^14.1.0 || ^13.0.0" } }, + "node_modules/mongodb-memory-server": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-11.0.1.tgz", + "integrity": "sha512-nUlKovSJZBh7q5hPsewFRam9H66D08Ne18nyknkNalfXMPtK1Og3kOcuqQhcX88x/pghSZPIJHrLbxNFW3OWiw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mongodb-memory-server-core": "11.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-11.0.1.tgz", + "integrity": "sha512-IcIb2S9Xf7Lmz43Z1ZujMqNg7PU5Q7yn+4wOnu7l6pfeGPkEmlqzV1hIbroVx8s4vXhPB1oMGC1u8clW7aj3Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-mutex": "^0.5.0", + "camelcase": "^6.3.0", + "debug": "^4.4.3", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.11", + "https-proxy-agent": "^7.0.6", + "mongodb": "^7.0.0", + "new-find-package-json": "^2.0.0", + "semver": "^7.7.3", + "tar-stream": "^3.1.7", + "tslib": "^2.8.1", + "yauzl": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.1.tgz", + "integrity": "sha512-067DXiMjcpYQl6bGjWQoTUEE9UoRViTtKFcoqX7z08I+iDZv/emH1g8XEFiO3qiDfXAheT5ozl1VffDTKhIW/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.1.1", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "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-memory-server-core/node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/mongoose": { "version": "8.19.1", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", @@ -12847,6 +13204,19 @@ "dev": true, "license": "MIT" }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, "node_modules/next": { "version": "15.5.7", "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", @@ -13422,6 +13792,13 @@ "dev": true, "license": "ISC" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -13651,6 +14028,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -13681,6 +14059,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13956,6 +14335,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13974,6 +14354,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14002,6 +14383,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.64.0.tgz", "integrity": "sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14923,6 +15305,18 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-event-emitter": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz", @@ -15342,7 +15736,8 @@ "version": "4.1.14", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -15384,6 +15779,44 @@ "node": ">=18" } }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar-stream/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -15421,6 +15854,31 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", @@ -15471,6 +15929,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15591,6 +16050,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15794,6 +16254,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16631,6 +17092,20 @@ "node": ">=8" } }, + "node_modules/yauzl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yjs": { "version": "13.6.27", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", @@ -16677,6 +17152,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index f6b6a9f..c421bb8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,13 @@ "lint": "next lint", "test": "jest", "test:watch": "jest --watch", - "test:coverage": "jest --coverage" + "test:unit": "jest --selectProjects client", + "test:integration": "jest --selectProjects server", + "test:coverage": "jest --coverage", + "test:actions": "jest --testPathPatterns=tests/integration/actions", + "test:api": "jest --testPathPatterns=tests/integration/api", + "test:watch:integration": "jest --watch --selectProjects server", + "test:coverage:integration": "jest --coverage --selectProjects server" }, "dependencies": { "@ai-sdk/openai": "^2.0.47", @@ -68,6 +74,7 @@ "eslint-plugin-prettier": "^5.5.4", "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", + "mongodb-memory-server": "^11.0.1", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4", diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..22e6208 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,90 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/tests/config/db-integration.ts b/tests/config/db-integration.ts new file mode 100644 index 0000000..22f41df --- /dev/null +++ b/tests/config/db-integration.ts @@ -0,0 +1,74 @@ +import { MongoMemoryReplSet } from "mongodb-memory-server"; +import mongoose from "mongoose"; + +let mongoServer: MongoMemoryReplSet; +let isConnected = false; + +export async function connectDB(): Promise { + if (isConnected) { + return; + } + + try { + mongoServer = await MongoMemoryReplSet.create({ + replSet: { count: 1, storageEngine: "wiredTiger" }, + }); + const uri = mongoServer.getUri(); + + await mongoose.connect(uri, { + dbName: "testdb", + }); + + if (global.mongoose) { + global.mongoose.conn = mongoose; + global.mongoose.promise = Promise.resolve(mongoose); + } + + isConnected = true; + } catch (error) { + console.error("Failed to connect to integration DB:", error); + throw error; + } +} + +export async function disconnectDB(): Promise { + if (!isConnected) { + return; + } + + try { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + + if (mongoServer) { + await mongoServer.stop(); + } + + if (global.mongoose) { + global.mongoose.conn = null; + global.mongoose.promise = null; + } + + isConnected = false; + } catch (error) { + console.error("Error disconnecting integration DB:", error); + throw error; + } +} + +export async function clearDB(): Promise { + if (!isConnected) { + throw new Error("Database not connected"); + } + + try { + await mongoose.connection.dropDatabase(); + } catch (error) { + console.error("Error clearing integration DB:", error); + throw error; + } +} + +export function isDBConnected(): boolean { + return isConnected && mongoose.connection.readyState === 1; +} diff --git a/tests/integration/actions/getTags.server.test.ts b/tests/integration/actions/getTags.server.test.ts new file mode 100644 index 0000000..98d880b --- /dev/null +++ b/tests/integration/actions/getTags.server.test.ts @@ -0,0 +1,235 @@ +import { Question, Tag, User } from "@/database"; +import { ITagDoc } from "@/database/tag.model"; +import { IUserDoc } from "@/database/user.model"; +import { getQuestions } from "@/lib/actions/question.action"; + +describe("getQuestions action", () => { + describe("validation", () => { + it("should return error for invalid params", async () => { + const invalidParams = { + page: "invalid", + pageSize: -5, + } as unknown as PaginatedSearchParams; + const result = await getQuestions(invalidParams); + + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + expect(result.error && result.error.message).toContain( + "Invalid input: expected number, received string, Page size must be at least 1" + ); + }); + }); + + describe("Pagination and Sorting", () => { + let testUser: IUserDoc; + let testTags: ITagDoc[]; + + beforeEach(async () => { + // Create test user + testUser = await User.create({ + name: "Test User", + username: "testuser", + email: "test@example.com", + }); + + // Create test tags + testTags = await Tag.insertMany([ + { name: "javascript", questions: 0 }, + { name: "react", questions: 0 }, + { name: "node", questions: 0 }, + ]); + + // Create test questions + const testQuestions = [ + { + title: "How to use React hooks?", + content: "I need help with React hooks", + author: testUser._id, + tags: [testTags[1]._id], + views: 100, + upvotes: 50, + answers: 5, + createdAt: new Date("2024-01-01"), + }, + { + title: "JavaScript async/await explained", + content: "Can someone explain async/await?", + author: testUser._id, + tags: [testTags[0]._id], + views: 200, + upvotes: 100, + answers: 0, + createdAt: new Date("2024-02-01"), + }, + { + title: "Node.js best practices", + content: "What are the best practices for Node.js?", + author: testUser._id, + tags: [testTags[2]._id], + views: 150, + upvotes: 75, + answers: 3, + createdAt: new Date("2024-03-01"), + }, + ]; + await Question.insertMany(testQuestions); + }); + + afterEach(async () => { + await Question.deleteMany({}); + await Tag.deleteMany({}); + await User.deleteMany({}); + }); + + it("should return the first page of questions sorted by creation date (default behavior)", async () => { + const result = await getQuestions({ page: 1, pageSize: 2 }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(2); + expect(result.data?.questions[0].title).toBe("Node.js best practices"); + expect(result.data?.questions[1].title).toBe("JavaScript async/await explained"); + expect(result.data?.isNext).toBe(true); + }); + + it("should return the second page of questions when paginated", async () => { + const result = await getQuestions({ page: 2, pageSize: 2 }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(1); + expect(result.data?.questions[0].title).toBe("How to use React hooks?"); + expect(result.data?.isNext).toBe(false); + }); + + it("should sort questions by newest when filter is 'newest'", async () => { + const result = await getQuestions({ + page: 1, + pageSize: 10, + filter: "newest", + }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(3); + expect(result.data?.questions[0].title).toBe("Node.js best practices"); + expect(result.data?.questions[1].title).toBe("JavaScript async/await explained"); + expect(result.data?.questions[2].title).toBe("How to use React hooks?"); + }); + + it("should filter and sort unanswered questions when filter is 'unanswered'", async () => { + const result = await getQuestions({ + page: 1, + pageSize: 10, + filter: "unanswered", + }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(1); + expect(result.data?.questions[0].title).toBe("JavaScript async/await explained"); + expect(result.data?.questions[0].answers).toBe(0); + }); + + it("should sort questions by upvotes when filter is 'popular'", async () => { + const result = await getQuestions({ + page: 1, + pageSize: 10, + filter: "popular", + }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(3); + expect(result.data?.questions[0].title).toBe("JavaScript async/await explained"); + expect(result.data?.questions[0].upvotes).toBe(100); + expect(result.data?.questions[1].upvotes).toBe(75); + expect(result.data?.questions[2].upvotes).toBe(50); + }); + }); + + describe("Search Functionality", () => { + let testUser: IUserDoc; + let testTag: ITagDoc; + + beforeEach(async () => { + testUser = await User.create({ + name: "Test User", + username: "testuser", + email: "test@example.com", + }); + + testTag = await Tag.create({ name: "javascript", questions: 0 }); + + await Question.insertMany([ + { + title: "JavaScript array methods", + content: "How to use map, filter, and reduce?", + author: testUser._id, + tags: [testTag._id], + }, + { + title: "React hooks tutorial", + content: "Learn about useState and useEffect", + author: testUser._id, + tags: [testTag._id], + }, + { + title: "Python data structures", + content: "Understanding lists and dictionaries", + author: testUser._id, + tags: [testTag._id], + }, + ]); + }); + + afterEach(async () => { + await Question.deleteMany({}); + await Tag.deleteMany({}); + await User.deleteMany({}); + }); + + it("should filter questions by title match (case-insensitive)", async () => { + const result = await getQuestions({ + page: 1, + pageSize: 10, + query: "javascript", + }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(1); + expect(result.data?.questions[0].title).toBe("JavaScript array methods"); + }); + + it("should filter questions by content match (case-insensitive)", async () => { + const result = await getQuestions({ + page: 1, + pageSize: 10, + query: "useState", + }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(1); + expect(result.data?.questions[0].title).toBe("React hooks tutorial"); + }); + + it("should return an empty array when no questions match the query", async () => { + const result = await getQuestions({ + page: 1, + pageSize: 10, + query: "nonexistent", + }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(0); + }); + + it("should search and filter together", async () => { + const result = await getQuestions({ + page: 1, + pageSize: 10, + query: "react", + filter: "newest", + }); + + expect(result.success).toBe(true); + expect(result.data?.questions).toHaveLength(1); + expect(result.data?.questions[0].title).toBe("React hooks tutorial"); + }); + }); +}); diff --git a/tests/integration/actions/vote.server.test.ts b/tests/integration/actions/vote.server.test.ts new file mode 100644 index 0000000..dc2135d --- /dev/null +++ b/tests/integration/actions/vote.server.test.ts @@ -0,0 +1,157 @@ +import { Question, User, Vote } from "@/database"; +import { createVote } from "@/lib/actions/vote.action"; +import { mockAuth } from "@/tests/mocks"; + +jest.mock("next/cache", () => ({ + revalidatePath: jest.fn(), +})); + +jest.mock("next/server", () => ({ + after: jest.fn((callback: () => void) => callback()), +})); + +describe("createVote action", () => { + describe("validation", () => { + beforeEach(async () => { + const testUser = await User.create({ + name: "Test User", + username: "testuser", + email: "test@example.com", + }); + + await Question.create({ + title: "Test Question", + content: "This is a test question", + author: testUser._id, + tags: [], + }); + + mockAuth.mockResolvedValue({ + user: { + id: testUser._id.toString(), + name: testUser.name, + email: testUser.email, + }, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }); + }); + + afterEach(async () => { + await Vote.deleteMany({}); + await Question.deleteMany({}); + await User.deleteMany({}); + jest.clearAllMocks(); + }); + + it("should return error for invalid targetId", async () => { + const result = await createVote({ + targetId: "invalid-id", + targetType: "question", + voteType: "upvote", + }); + + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + }); + + it("should return error when user is not authenticated", async () => { + mockAuth.mockResolvedValueOnce(null); + + const testQuestion = await Question.findOne(); + const result = await createVote({ + targetId: testQuestion!._id.toString(), + targetType: "question", + voteType: "upvote", + }); + + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + expect(result.error && result.error.message).toContain("Unauthorized"); + }); + }); + + describe("Vote Creation", () => { + beforeEach(async () => { + const testUser = await User.create({ + name: "Test User", + username: "testuser", + email: "test@example.com", + }); + + await Question.create({ + title: "Test Question", + content: "This is a test question", + author: testUser._id, + tags: [], + }); + + mockAuth.mockResolvedValue({ + user: { + id: testUser._id.toString(), + name: testUser.name, + email: testUser.email, + }, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }); + }); + + afterEach(async () => { + await Vote.deleteMany({}); + await Question.deleteMany({}); + await User.deleteMany({}); + jest.clearAllMocks(); + }); + + it("should create an upvote on a question and increment upvote count", async () => { + const testQuestion = await Question.findOne(); + const testUser = await User.findOne(); + + const result = await createVote({ + targetId: testQuestion!._id.toString(), + targetType: "question", + voteType: "upvote", + }); + + expect(result.success).toBe(true); + + const vote = await Vote.findOne({ + author: testUser!._id, + actionId: testQuestion!._id, + actionType: "question", + }); + + expect(vote).toBeDefined(); + expect(vote?.voteType).toBe("upvote"); + + const updatedQuestion = await Question.findById(testQuestion!._id); + expect(updatedQuestion?.upvotes).toBe(1); + expect(updatedQuestion?.downvotes).toBe(0); + }); + + it("should create a downvote on a question and increment downvote count", async () => { + const testQuestion = await Question.findOne(); + const testUser = await User.findOne(); + + const result = await createVote({ + targetId: testQuestion!._id.toString(), + targetType: "question", + voteType: "downvote", + }); + + expect(result.success).toBe(true); + + const vote = await Vote.findOne({ + author: testUser!._id, + actionId: testQuestion!._id, + actionType: "question", + }); + + expect(vote).toBeDefined(); + expect(vote?.voteType).toBe("downvote"); + + const updatedQuestion = await Question.findById(testQuestion!._id); + expect(updatedQuestion?.upvotes).toBe(0); + expect(updatedQuestion?.downvotes).toBe(1); + }); + }); +}); diff --git a/tests/mocks/metric.mock.tsx b/tests/mocks/metric.mock.tsx index a56675a..be6a329 100644 --- a/tests/mocks/metric.mock.tsx +++ b/tests/mocks/metric.mock.tsx @@ -10,7 +10,7 @@ interface MetricProps { const MockMetric = ({ imgUrl, alt, value, title, textStyles }: MetricProps) => { return ( -
+
{value} {title}
diff --git a/tests/mocks/nextauth.mock.ts b/tests/mocks/nextauth.mock.ts index 68d935e..f4d6ba3 100644 --- a/tests/mocks/nextauth.mock.ts +++ b/tests/mocks/nextauth.mock.ts @@ -1,4 +1,6 @@ -const mockSession = { +import type { Session } from "next-auth"; + +const mockSession: Session = { user: { id: "mock-user-123", name: "Test User", @@ -8,7 +10,7 @@ const mockSession = { expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), }; -const mockAuth = jest.fn(); +const mockAuth = jest.fn, []>(); const mockSignIn = jest.fn(); const mockSignOut = jest.fn(); diff --git a/tests/unit/components/questioncard.test.tsx b/tests/unit/components/questioncard.test.tsx index 88974f5..69c4f87 100644 --- a/tests/unit/components/questioncard.test.tsx +++ b/tests/unit/components/questioncard.test.tsx @@ -67,4 +67,26 @@ describe("QuestionCard Component", () => { }); }); }); + + it("should show timestamp on large screens", () => { + // Simulate a large screen + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 800, + }); + window.dispatchEvent(new Event("resize")); + + render(); + + // Check timestamp visibility + const timestampFlex = screen.getByText(relativeTimeText, { + selector: "span", + }); + expect(timestampFlex).toBeVisible(); + + // Check metrics visibility + const metric = screen.getAllByTestId("metric")[0]; + expect(metric).toBeVisible(); + }); });