Skip to content

Commit c8bd8f5

Browse files
committed
test(web): add unit tests for Loom import deduplication and stale rows
Made-with: Cursor
1 parent a9e6163 commit c8bd8f5

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const whereMock = vi.fn();
4+
const valuesMock = vi.fn();
5+
const startMock = vi.fn();
6+
const revalidatePathMock = vi.fn();
7+
8+
const mockDb = {
9+
select: vi.fn(() => mockDb),
10+
insert: vi.fn(() => mockDb),
11+
delete: vi.fn(() => mockDb),
12+
from: vi.fn(() => mockDb),
13+
leftJoin: vi.fn(() => mockDb),
14+
where: whereMock,
15+
values: valuesMock,
16+
};
17+
18+
vi.mock("@cap/database", () => ({
19+
db: vi.fn(() => mockDb),
20+
}));
21+
22+
vi.mock("@cap/database/auth/session", () => ({
23+
getCurrentUser: vi.fn(),
24+
}));
25+
26+
vi.mock("@cap/database/helpers", () => ({
27+
nanoId: vi.fn(() => "video-123"),
28+
}));
29+
30+
vi.mock("@cap/database/schema", () => ({
31+
importedVideos: {
32+
id: "id",
33+
orgId: "orgId",
34+
source: "source",
35+
sourceId: "sourceId",
36+
},
37+
s3Buckets: {
38+
id: "id",
39+
ownerId: "ownerId",
40+
},
41+
videos: {
42+
id: "id",
43+
orgId: "orgId",
44+
},
45+
videoUploads: {
46+
videoId: "videoId",
47+
},
48+
}));
49+
50+
vi.mock("@cap/env", () => ({
51+
buildEnv: { NEXT_PUBLIC_IS_CAP: false },
52+
NODE_ENV: "test",
53+
serverEnv: vi.fn(() => ({
54+
CAP_VIDEOS_DEFAULT_PUBLIC: true,
55+
WEB_URL: "https://cap.test",
56+
})),
57+
}));
58+
59+
vi.mock("@cap/utils", () => ({
60+
dub: vi.fn(() => ({
61+
links: {
62+
create: vi.fn(),
63+
},
64+
})),
65+
userIsPro: vi.fn(() => true),
66+
}));
67+
68+
vi.mock("@cap/web-domain", () => ({
69+
Video: {
70+
VideoId: {
71+
make: vi.fn((value: string) => value),
72+
},
73+
},
74+
}));
75+
76+
vi.mock("drizzle-orm", () => ({
77+
and: vi.fn((...args: unknown[]) => args),
78+
eq: vi.fn((field: unknown, value: unknown) => ({ field, value })),
79+
}));
80+
81+
vi.mock("next/cache", () => ({
82+
revalidatePath: revalidatePathMock,
83+
}));
84+
85+
vi.mock("workflow/api", () => ({
86+
start: startMock,
87+
}));
88+
89+
vi.mock("@/workflows/import-loom-video", () => ({
90+
importLoomVideoWorkflow: Symbol("importLoomVideoWorkflow"),
91+
}));
92+
93+
import { getCurrentUser } from "@cap/database/auth/session";
94+
95+
const mockGetCurrentUser = getCurrentUser as ReturnType<typeof vi.fn>;
96+
97+
describe("importFromLoom", () => {
98+
beforeEach(() => {
99+
vi.clearAllMocks();
100+
whereMock.mockReset();
101+
valuesMock.mockReset();
102+
mockDb.select.mockReturnValue(mockDb);
103+
mockDb.insert.mockReturnValue(mockDb);
104+
mockDb.delete.mockReturnValue(mockDb);
105+
mockDb.from.mockReturnValue(mockDb);
106+
mockDb.leftJoin.mockReturnValue(mockDb);
107+
valuesMock.mockResolvedValue(undefined);
108+
whereMock.mockResolvedValue([]);
109+
startMock.mockResolvedValue(undefined);
110+
mockGetCurrentUser.mockResolvedValue({
111+
id: "user-123",
112+
});
113+
vi.stubGlobal("fetch", vi.fn());
114+
});
115+
116+
it("rejects a Loom import when the linked Cap still exists", async () => {
117+
whereMock.mockResolvedValueOnce([
118+
{ importedVideoId: "video-123", videoId: "video-123" },
119+
]);
120+
121+
const fetchMock = vi.mocked(fetch);
122+
const { importFromLoom } = await import("@/actions/loom");
123+
124+
const result = await importFromLoom({
125+
loomUrl: "https://www.loom.com/share/loom-abc1234567",
126+
orgId: "org-1" as never,
127+
});
128+
129+
expect(result).toEqual({
130+
success: false,
131+
error: "This Loom video has already been imported.",
132+
});
133+
expect(fetchMock).not.toHaveBeenCalled();
134+
expect(valuesMock).not.toHaveBeenCalled();
135+
});
136+
137+
it("removes a stale Loom row and recreates it with the Cap video id", async () => {
138+
whereMock
139+
.mockResolvedValueOnce([{ importedVideoId: "stale-row", videoId: null }])
140+
.mockResolvedValueOnce([{ id: "bucket-1" }])
141+
.mockResolvedValueOnce(undefined);
142+
143+
const fetchMock = vi.mocked(fetch);
144+
fetchMock.mockImplementation(async (input) => {
145+
const url = typeof input === "string" ? input : input.toString();
146+
147+
if (url.includes("/transcoded-url")) {
148+
return {
149+
ok: true,
150+
status: 200,
151+
text: async () =>
152+
JSON.stringify({ url: "https://cdn.loom.com/video.mp4" }),
153+
} as Response;
154+
}
155+
156+
if (url === "https://www.loom.com/graphql") {
157+
return {
158+
ok: true,
159+
json: async () => ({
160+
data: { getVideo: { name: "Imported video" } },
161+
}),
162+
} as Response;
163+
}
164+
165+
if (url.includes("/v1/oembed")) {
166+
return {
167+
ok: true,
168+
json: async () => ({ duration: 42, width: 1920, height: 1080 }),
169+
} as Response;
170+
}
171+
172+
throw new Error(`Unexpected fetch: ${url}`);
173+
});
174+
175+
const { importFromLoom } = await import("@/actions/loom");
176+
177+
const result = await importFromLoom({
178+
loomUrl: "https://www.loom.com/share/loom-abc1234567",
179+
orgId: "org-1" as never,
180+
});
181+
182+
expect(result).toEqual({
183+
success: true,
184+
videoId: "video-123",
185+
});
186+
expect(mockDb.delete).toHaveBeenCalledTimes(1);
187+
expect(valuesMock).toHaveBeenCalledTimes(3);
188+
expect(valuesMock).toHaveBeenNthCalledWith(
189+
3,
190+
expect.objectContaining({
191+
id: "video-123",
192+
orgId: "org-1",
193+
source: "loom",
194+
sourceId: "loom-abc1234567",
195+
}),
196+
);
197+
expect(startMock).toHaveBeenCalledTimes(1);
198+
expect(revalidatePathMock).toHaveBeenCalledWith("/dashboard/caps");
199+
});
200+
});

0 commit comments

Comments
 (0)