From e439a98a90944f3eacdb458785b4bb5457645143 Mon Sep 17 00:00:00 2001 From: Tehlikeli107 Date: Fri, 15 May 2026 02:16:34 +0300 Subject: [PATCH] test: add web-backend vitest coverage --- packages/web-backend/package.json | 6 +- .../src/Storage/GoogleDrive.test.ts | 17 ++++ .../src/Storage/SignedObject.test.ts | 90 +++++++++++++++++++ .../src/Videos/EffectiveVideoRules.test.ts | 85 ++++++++++++++++++ packages/web-backend/vitest.config.ts | 9 ++ pnpm-lock.yaml | 4 + 6 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 packages/web-backend/src/Storage/GoogleDrive.test.ts create mode 100644 packages/web-backend/src/Storage/SignedObject.test.ts create mode 100644 packages/web-backend/src/Videos/EffectiveVideoRules.test.ts create mode 100644 packages/web-backend/vitest.config.ts diff --git a/packages/web-backend/package.json b/packages/web-backend/package.json index 6d644700853..97a1137ade3 100644 --- a/packages/web-backend/package.json +++ b/packages/web-backend/package.json @@ -8,7 +8,8 @@ "main": "./dist/index.js" }, "scripts": { - "build": "tsdown" + "build": "tsdown", + "test": "vitest run" }, "dependencies": { "@cap/env": "workspace:*", @@ -30,5 +31,8 @@ "effect": "^3.18.4", "next": "15.5.9", "server-only": "^0.0.1" + }, + "devDependencies": { + "vitest": "~2.1.9" } } diff --git a/packages/web-backend/src/Storage/GoogleDrive.test.ts b/packages/web-backend/src/Storage/GoogleDrive.test.ts new file mode 100644 index 00000000000..e83a0f097fc --- /dev/null +++ b/packages/web-backend/src/Storage/GoogleDrive.test.ts @@ -0,0 +1,17 @@ +import { Option } from "effect"; +import { describe, expect, it } from "vitest"; +import { parseVideoIdFromObjectKey } from "./GoogleDrive.ts"; + +describe("parseVideoIdFromObjectKey", () => { + it("extracts the video id from user/video object keys", () => { + const result = parseVideoIdFromObjectKey("owner-1/video-1/source.mp4"); + + expect(Option.isSome(result)).toBe(true); + expect(result.pipe(Option.getOrElse(() => ""))).toBe("video-1"); + }); + + it("returns none when the object key does not include a video segment", () => { + expect(Option.isNone(parseVideoIdFromObjectKey("owner-1"))).toBe(true); + expect(Option.isNone(parseVideoIdFromObjectKey("owner-1/"))).toBe(true); + }); +}); diff --git a/packages/web-backend/src/Storage/SignedObject.test.ts b/packages/web-backend/src/Storage/SignedObject.test.ts new file mode 100644 index 00000000000..1ab500322aa --- /dev/null +++ b/packages/web-backend/src/Storage/SignedObject.test.ts @@ -0,0 +1,90 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const REQUIRED_ENV = { + DATABASE_URL: "mysql://user:password@localhost:3306/cap", + WEB_URL: "http://localhost:3000", + NEXTAUTH_SECRET: "test-nextauth-secret", + NEXTAUTH_URL: "http://localhost:3000", + CAP_AWS_BUCKET: "test-bucket", + CAP_AWS_REGION: "us-east-1", + NODE_ENV: "test", +}; + +function setRequiredEnv() { + for (const [key, value] of Object.entries(REQUIRED_ENV)) { + process.env[key] = value; + } +} + +function clearRequiredEnv() { + for (const key of Object.keys(REQUIRED_ENV)) { + delete process.env[key]; + } +} + +async function importSignedObject() { + vi.resetModules(); + setRequiredEnv(); + return import("./SignedObject.ts"); +} + +describe("storage object tokens", () => { + beforeEach(() => { + setRequiredEnv(); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-05-15T00:00:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + clearRequiredEnv(); + }); + + it("creates tokens that verify back to their storage payload", async () => { + const { createStorageObjectToken, verifyStorageObjectToken } = + await importSignedObject(); + + const token = createStorageObjectToken( + { videoId: "video-1", key: "owner-1/video-1/source.mp4" }, + 60, + ); + + expect(verifyStorageObjectToken(token)).toEqual({ + videoId: "video-1", + key: "owner-1/video-1/source.mp4", + expiresAt: Date.parse("2026-05-15T00:01:00.000Z"), + }); + }); + + it("rejects tokens when the signature is changed", async () => { + const { createStorageObjectToken, verifyStorageObjectToken } = + await importSignedObject(); + + const token = createStorageObjectToken({ + videoId: "video-1", + key: "owner-1/video-1/source.mp4", + }); + const [payload, signature] = token.split("."); + const changedSignature = `${signature?.slice(0, -1)}${ + signature?.endsWith("A") ? "B" : "A" + }`; + + expect( + verifyStorageObjectToken(`${payload}.${changedSignature}`), + ).toBeNull(); + }); + + it("rejects tokens after their expiry time", async () => { + const { createStorageObjectToken, verifyStorageObjectToken } = + await importSignedObject(); + + const token = createStorageObjectToken( + { videoId: "video-1", key: "owner-1/video-1/source.mp4" }, + 60, + ); + + vi.advanceTimersByTime(60_001); + + expect(verifyStorageObjectToken(token)).toBeNull(); + }); +}); diff --git a/packages/web-backend/src/Videos/EffectiveVideoRules.test.ts b/packages/web-backend/src/Videos/EffectiveVideoRules.test.ts new file mode 100644 index 00000000000..0b8705df658 --- /dev/null +++ b/packages/web-backend/src/Videos/EffectiveVideoRules.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { + collectPasswordHashes, + resolveEffectiveVideoRules, +} from "./EffectiveVideoRules.ts"; + +describe("resolveEffectiveVideoRules", () => { + it("defaults every viewer setting to enabled when no restrictions exist", () => { + const rules = resolveEffectiveVideoRules({ + videoSettings: null, + organizationSettings: null, + spaces: [], + }); + + expect(rules.settings).toEqual({ + disableSummary: false, + disableCaptions: false, + disableChapters: false, + disableReactions: false, + disableTranscript: false, + disableComments: false, + }); + expect(rules.inheritedSettings).toEqual({}); + expect(rules.hasInheritedPassword).toBe(false); + }); + + it("records every space that forces the same inherited setting", () => { + const rules = resolveEffectiveVideoRules({ + videoSettings: { disableComments: false }, + organizationSettings: { disableComments: false }, + spaces: [ + { + id: "space-1", + name: "Legal", + settings: { disableComments: true }, + }, + { + id: "space-2", + name: "Customer Success", + settings: { disableComments: true }, + }, + ], + }); + + expect(rules.settings.disableComments).toBe(true); + expect(rules.inheritedSettings.disableComments).toEqual([ + { id: "space-1", name: "Legal" }, + { id: "space-2", name: "Customer Success" }, + ]); + }); + + it("treats either explicit password metadata or stored password hashes as inherited passwords", () => { + const rules = resolveEffectiveVideoRules({ + videoSettings: {}, + organizationSettings: {}, + spaces: [ + { id: "space-1", name: "Recorded Training", hasPassword: true }, + { id: "space-2", name: "Sales", password: "space-password-hash" }, + { id: "space-3", name: "Open", hasPassword: false, password: "" }, + ], + }); + + expect(rules.hasInheritedPassword).toBe(true); + expect(rules.inheritedPasswordSources).toEqual([ + { id: "space-1", name: "Recorded Training" }, + { id: "space-2", name: "Sales" }, + ]); + }); +}); + +describe("collectPasswordHashes", () => { + it("preserves the video password first and skips empty inherited passwords", () => { + expect( + collectPasswordHashes({ + videoPassword: "video-password-hash", + spacePasswords: [ + { password: "space-password-hash" }, + { password: "" }, + { password: null }, + {}, + ], + }), + ).toEqual(["video-password-hash", "space-password-hash"]); + }); +}); diff --git a/packages/web-backend/vitest.config.ts b/packages/web-backend/vitest.config.ts new file mode 100644 index 00000000000..b9379103131 --- /dev/null +++ b/packages/web-backend/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + exclude: ["**/node_modules/**", "**/dist/**"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce77b06b4f8..0770e845462 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1401,6 +1401,10 @@ importers: server-only: specifier: ^0.0.1 version: 0.0.1 + devDependencies: + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/web-domain: dependencies: