From bd1bdfdccc22afe416196824d1e4a3a0b644cec1 Mon Sep 17 00:00:00 2001 From: sypham98-prog <246709336+sypham98-prog@users.noreply.github.com> Date: Sun, 17 May 2026 23:45:53 +0700 Subject: [PATCH 1/2] test: add env and database vitest coverage --- packages/database/crypto.test.ts | 24 +++++ packages/database/package.json | 6 +- packages/database/vitest.config.ts | 8 ++ packages/env/build.test.ts | 38 ++++++++ packages/env/package.json | 6 +- packages/env/server.test.ts | 48 +++++++++ packages/env/vitest.config.ts | 8 ++ pnpm-lock.yaml | 150 +++++++++++++++++++++++++++++ 8 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 packages/database/crypto.test.ts create mode 100644 packages/database/vitest.config.ts create mode 100644 packages/env/build.test.ts create mode 100644 packages/env/server.test.ts create mode 100644 packages/env/vitest.config.ts diff --git a/packages/database/crypto.test.ts b/packages/database/crypto.test.ts new file mode 100644 index 00000000000..4951099b210 --- /dev/null +++ b/packages/database/crypto.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { hashPassword, verifyPassword } from "./crypto"; + +describe("password hashing", () => { + it("verifies the original password and rejects a different one", async () => { + const hash = await hashPassword("correct horse battery staple"); + + expect(hash).not.toBe("correct horse battery staple"); + expect(await verifyPassword(hash, "correct horse battery staple")).toBe( + true, + ); + expect(await verifyPassword(hash, "wrong password")).toBe(false); + }); + + it("rejects empty hash and password inputs", async () => { + const hash = await hashPassword("non-empty password"); + + expect(await verifyPassword("", "non-empty password")).toBe(false); + expect(await verifyPassword(hash, "")).toBe(false); + await expect(hashPassword("")).rejects.toThrow( + "Cannot hash empty or null password", + ); + }); +}); diff --git a/packages/database/package.json b/packages/database/package.json index a221a469a38..9f8f2ef9880 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -13,7 +13,8 @@ "db:studio": "drizzle-kit studio --config=drizzle.config.ts", "db:check-integrity": "node scripts/check-migration-integrity.js", "drizzle-kit": "pnpm dotenv -e ../../.env drizzle-kit --config=drizzle.config.ts", - "build": "tsdown" + "build": "tsdown", + "test": "vitest run" }, "dependencies": { "@cap/env": "workspace:*", @@ -50,7 +51,8 @@ "react-dom": "^19.1.1", "react-router-dom": "^6.18.0", "tsconfig": "workspace:*", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "~2.1.9" }, "engines": { "node": ">=20" diff --git a/packages/database/vitest.config.ts b/packages/database/vitest.config.ts new file mode 100644 index 00000000000..ce63ad84542 --- /dev/null +++ b/packages/database/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["**/*.test.ts"], + }, +}); diff --git a/packages/env/build.test.ts b/packages/env/build.test.ts new file mode 100644 index 00000000000..f6e4499050b --- /dev/null +++ b/packages/env/build.test.ts @@ -0,0 +1,38 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const originalEnv = { ...process.env }; + +function resetEnv() { + for (const key of Object.keys(process.env)) { + delete process.env[key]; + } + Object.assign(process.env, originalEnv); +} + +afterEach(() => { + resetEnv(); + vi.resetModules(); +}); + +describe("buildEnv", () => { + it("uses WEB_URL as the public web URL fallback", async () => { + process.env.WEB_URL = "https://cap.example"; + delete process.env.NEXT_PUBLIC_WEB_URL; + + const { buildEnv } = await import("./build"); + + expect(buildEnv.NEXT_PUBLIC_WEB_URL).toBe("https://cap.example"); + }); + + it("caches the parsed environment after first access", async () => { + process.env.WEB_URL = "https://first.example"; + + const { buildEnv } = await import("./build"); + + expect(buildEnv.NEXT_PUBLIC_WEB_URL).toBe("https://first.example"); + + process.env.WEB_URL = "https://second.example"; + + expect(buildEnv.NEXT_PUBLIC_WEB_URL).toBe("https://first.example"); + }); +}); diff --git a/packages/env/package.json b/packages/env/package.json index 926c67f70b7..635593920e8 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -5,7 +5,8 @@ "main": "./index.ts", "types": "./index.ts", "scripts": { - "build": "tsdown" + "build": "tsdown", + "test": "vitest run" }, "publishConfig": { "main": "./dist/index.js" @@ -15,6 +16,7 @@ "zod": "^3.25.76" }, "devDependencies": { - "@types/node": "^22.15.14" + "@types/node": "^22.15.14", + "vitest": "~2.1.9" } } diff --git a/packages/env/server.test.ts b/packages/env/server.test.ts new file mode 100644 index 00000000000..3fa213760a8 --- /dev/null +++ b/packages/env/server.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const originalEnv = { ...process.env }; + +function resetEnv() { + for (const key of Object.keys(process.env)) { + delete process.env[key]; + } + Object.assign(process.env, originalEnv); +} + +function setRequiredServerEnv() { + process.env.DATABASE_URL = "mysql://user:password@localhost:3306/cap"; + process.env.WEB_URL = "https://cap.example"; + process.env.NEXTAUTH_SECRET = "test-secret"; + process.env.NEXTAUTH_URL = "https://cap.example"; + process.env.CAP_AWS_BUCKET = "cap-test-bucket"; + process.env.CAP_AWS_REGION = "us-east-1"; + process.env.NODE_ENV = "test"; +} + +afterEach(() => { + resetEnv(); + vi.resetModules(); +}); + +describe("serverEnv", () => { + it("maps CAP_AWS_ENDPOINT to both S3 endpoint aliases", async () => { + setRequiredServerEnv(); + process.env.CAP_AWS_ENDPOINT = "https://s3.example"; + + const { serverEnv } = await import("./server"); + + const env = serverEnv(); + expect(env.S3_PUBLIC_ENDPOINT).toBe("https://s3.example"); + expect(env.S3_INTERNAL_ENDPOINT).toBe("https://s3.example"); + }); + + it("parses boolean string defaults for server settings", async () => { + setRequiredServerEnv(); + + const { serverEnv } = await import("./server"); + + const env = serverEnv(); + expect(env.S3_PATH_STYLE).toBe(true); + expect(env.CAP_VIDEOS_DEFAULT_PUBLIC).toBe(true); + }); +}); diff --git a/packages/env/vitest.config.ts b/packages/env/vitest.config.ts new file mode 100644 index 00000000000..ce63ad84542 --- /dev/null +++ b/packages/env/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1003cb70eb..6217bc0c949 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1064,6 +1064,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@20.17.43)(jsdom@26.1.0)(terser@5.44.0) packages/env: dependencies: @@ -1077,6 +1080,9 @@ importers: '@types/node': specifier: ^22.15.14 version: 22.15.14 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.14)(jsdom@26.1.0)(terser@5.44.0) packages/local-docker: {} @@ -24488,6 +24494,22 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@20.17.43)(terser@5.44.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@20.17.43)(terser@5.44.0) + + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.15.14)(terser@5.44.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@22.15.14)(terser@5.44.0) + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.15.17)(terser@5.44.0))': dependencies: '@vitest/spy': 2.1.9 @@ -33844,6 +33866,42 @@ snapshots: - xml2js - yaml + vite-node@2.1.9(@types/node@20.17.43)(terser@5.44.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@20.17.43)(terser@5.44.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@2.1.9(@types/node@22.15.14)(terser@5.44.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@22.15.14)(terser@5.44.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@2.1.9(@types/node@22.15.17)(terser@5.44.0): dependencies: cac: 6.7.14 @@ -33935,6 +33993,26 @@ snapshots: - supports-color - typescript + vite@5.4.19(@types/node@20.17.43)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.40.2 + optionalDependencies: + '@types/node': 20.17.43 + fsevents: 2.3.3 + terser: 5.44.0 + + vite@5.4.19(@types/node@22.15.14)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.40.2 + optionalDependencies: + '@types/node': 22.15.14 + fsevents: 2.3.3 + terser: 5.44.0 + vite@5.4.19(@types/node@22.15.17)(terser@5.44.0): dependencies: esbuild: 0.21.5 @@ -33991,6 +34069,78 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1) + vitest@2.1.9(@types/node@20.17.43)(jsdom@26.1.0)(terser@5.44.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@20.17.43)(terser@5.44.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.0 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@20.17.43)(terser@5.44.0) + vite-node: 2.1.9(@types/node@20.17.43)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.17.43 + jsdom: 26.1.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@2.1.9(@types/node@22.15.14)(jsdom@26.1.0)(terser@5.44.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@22.15.14)(terser@5.44.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.0 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@22.15.14)(terser@5.44.0) + vite-node: 2.1.9(@types/node@22.15.14)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.15.14 + jsdom: 26.1.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0): dependencies: '@vitest/expect': 2.1.9 From f8c9fcc4288feab3a733edbd2612b99c474bfd94 Mon Sep 17 00:00:00 2001 From: sypham98-prog <246709336+sypham98-prog@users.noreply.github.com> Date: Mon, 18 May 2026 00:37:48 +0700 Subject: [PATCH 2/2] test: isolate env boolean default tests --- packages/env/server.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/env/server.test.ts b/packages/env/server.test.ts index 3fa213760a8..ad7159e6f39 100644 --- a/packages/env/server.test.ts +++ b/packages/env/server.test.ts @@ -17,6 +17,8 @@ function setRequiredServerEnv() { process.env.CAP_AWS_BUCKET = "cap-test-bucket"; process.env.CAP_AWS_REGION = "us-east-1"; process.env.NODE_ENV = "test"; + delete process.env.S3_PATH_STYLE; + delete process.env.CAP_VIDEOS_DEFAULT_PUBLIC; } afterEach(() => {