Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/web-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"main": "./dist/index.js"
},
"scripts": {
"build": "tsdown"
"build": "tsdown",
"test": "vitest run"
},
"dependencies": {
"@cap/env": "workspace:*",
Expand All @@ -30,5 +31,8 @@
"effect": "^3.18.4",
"next": "15.5.9",
"server-only": "^0.0.1"
},
"devDependencies": {
"vitest": "~2.1.9"
}
}
17 changes: 17 additions & 0 deletions packages/web-backend/src/Storage/GoogleDrive.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
90 changes: 90 additions & 0 deletions packages/web-backend/src/Storage/SignedObject.test.ts
Original file line number Diff line number Diff line change
@@ -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"));
});
Comment on lines +32 to +36
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The setRequiredEnv() call in beforeEach is redundant: importSignedObject() always calls vi.resetModules() first (which clears @cap/env's cached state) and then calls setRequiredEnv() itself before the dynamic import. The beforeEach call fires before the module reset happens, so it's the import-time call inside importSignedObject() that's meaningful. Having the call in both places obscures which one is load-order critical.

Suggested change
beforeEach(() => {
setRequiredEnv();
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-15T00:00:00.000Z"));
});
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-15T00:00:00.000Z"));
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/web-backend/src/Storage/SignedObject.test.ts
Line: 32-36

Comment:
The `setRequiredEnv()` call in `beforeEach` is redundant: `importSignedObject()` always calls `vi.resetModules()` first (which clears `@cap/env`'s cached state) and then calls `setRequiredEnv()` itself before the dynamic import. The `beforeEach` call fires before the module reset happens, so it's the import-time call inside `importSignedObject()` that's meaningful. Having the call in both places obscures which one is load-order critical.

```suggestion
	beforeEach(() => {
		vi.useFakeTimers();
		vi.setSystemTime(new Date("2026-05-15T00:00:00.000Z"));
	});
```

How can I resolve this? If you propose a fix, please make it concise.


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"
}`;
Comment on lines +67 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 signature is always defined here because createStorageObjectToken always returns a string in the form encodedPayload.signature, and neither segment contains a .. The optional chaining (?.) is misleading — if signature were ever undefined, signature?.slice(0, -1) would evaluate to undefined, and the template literal would produce "undefinedA" or "undefinedB", which coincidentally would still differ from the real signature. Using a non-optional access makes the intent explicit.

Suggested change
const [payload, signature] = token.split(".");
const changedSignature = `${signature?.slice(0, -1)}${
signature?.endsWith("A") ? "B" : "A"
}`;
const [payload, signature] = token.split(".");
if (!payload || !signature) throw new Error("unexpected token format");
const changedSignature = `${signature.slice(0, -1)}${
signature.endsWith("A") ? "B" : "A"
}`;
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/web-backend/src/Storage/SignedObject.test.ts
Line: 67-70

Comment:
`signature` is always defined here because `createStorageObjectToken` always returns a string in the form `encodedPayload.signature`, and neither segment contains a `.`. The optional chaining (`?.`) is misleading — if `signature` were ever `undefined`, `signature?.slice(0, -1)` would evaluate to `undefined`, and the template literal would produce `"undefinedA"` or `"undefinedB"`, which coincidentally would still differ from the real signature. Using a non-optional access makes the intent explicit.

```suggestion
		const [payload, signature] = token.split(".");
		if (!payload || !signature) throw new Error("unexpected token format");
		const changedSignature = `${signature.slice(0, -1)}${
			signature.endsWith("A") ? "B" : "A"
		}`;
```

How can I resolve this? If you propose a fix, please make it concise.


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();
});
});
85 changes: 85 additions & 0 deletions packages/web-backend/src/Videos/EffectiveVideoRules.test.ts
Original file line number Diff line number Diff line change
@@ -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"]);
});
});
9 changes: 9 additions & 0 deletions packages/web-backend/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
environment: "node",
include: ["src/**/*.test.ts"],
exclude: ["**/node_modules/**", "**/dist/**"],
},
});
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.