Files
calendar/packages/features/url-shortener/__tests__/SinkClient.test.ts
T
Amit SharmaGitHubunknown <>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Keith Williams
353f71bdc6 feat: Sink url shortner for sms workflow reminders (#26608)
* feat: Sink url shortner for sms workflow reminders

* fix: remove hardcoded dub values

* update .env.example

* fix: unit tests

* chore: add tests for scheduleSmsReminder and utils

* review refactor

* fix: type check

* review refactor

* fix: update test to account for smsReminderNumber fallback from main

Co-Authored-By: unknown <>

* feat: add feature flag for sink and more tests to verify

* fix: type check

* use proper feature flags for sink

* Apply suggestion from @keithwillcode

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2026-03-16 20:51:28 +00:00

300 lines
8.4 KiB
TypeScript

import type { Mock } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SinkClient } from "../providers/SinkClient";
const mockFetch = vi.fn() as Mock;
global.fetch = mockFetch;
vi.mock("@calcom/lib/logger", () => ({
default: {
getSubLogger: () => ({
debug: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
}),
},
}));
describe("SinkClient", () => {
const mockSinkUrl = "https://sink.test.com";
const mockApiKey = "test-api-key";
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
});
describe("createLink", () => {
beforeEach(() => {
vi.stubEnv("SINK_API_URL", mockSinkUrl);
vi.stubEnv("SINK_API_KEY", mockApiKey);
});
it("should successfully create a shortened link", async () => {
const mockResponse = {
link: {
id: "abc123",
url: "https://example.com",
slug: "xyz",
createdAt: "2024-01-01",
updatedAt: "2024-01-01",
views: 0,
},
shortLink: "https://sink.test.com/xyz",
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const testSink = new SinkClient();
const result = await testSink.createLink("https://example.com");
expect(result).toBe("https://sink.test.com/xyz");
expect(global.fetch).toHaveBeenCalledWith(
`${mockSinkUrl}/api/link/create`,
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"Content-Type": "application/json",
Authorization: `Bearer ${mockApiKey}`,
}),
body: JSON.stringify({ url: "https://example.com" }),
})
);
});
it("should return null when Sink is not configured", async () => {
vi.stubEnv("SINK_API_URL", "");
const testSink = new SinkClient();
const result = await testSink.createLink("https://example.com");
expect(result).toBeNull();
expect(global.fetch).not.toHaveBeenCalled();
});
it("should return null on API error response", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
text: async () => "Internal Server Error",
});
const testSink = new SinkClient();
const result = await testSink.createLink("https://example.com");
expect(result).toBeNull();
});
it("should return null on network error", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network error"));
const testSink = new SinkClient();
const result = await testSink.createLink("https://example.com");
expect(result).toBeNull();
});
it("should pass options to the API", async () => {
const mockResponse = {
link: {
id: "abc123",
url: "https://example.com",
slug: "custom-slug",
comment: "Test comment",
createdAt: "2024-01-01",
updatedAt: "2024-01-01",
views: 0,
},
shortLink: "https://sink.test.com/custom-slug",
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const testSink = new SinkClient();
await testSink.createLink("https://example.com", {
slug: "custom-slug",
comment: "Test comment",
});
expect(global.fetch).toHaveBeenCalledWith(
`${mockSinkUrl}/api/link/create`,
expect.objectContaining({
body: JSON.stringify({
url: "https://example.com",
slug: "custom-slug",
comment: "Test comment",
}),
})
);
});
});
describe("createMany", () => {
beforeEach(() => {
vi.stubEnv("SINK_API_URL", mockSinkUrl);
vi.stubEnv("SINK_API_KEY", mockApiKey);
});
it("should successfully shorten multiple links", async () => {
const mockResponse = (url: string, index: number) => ({
link: {
id: `abc${index}`,
url,
slug: `xyz${index}`,
createdAt: "2024-01-01",
updatedAt: "2024-01-01",
views: 0,
},
shortLink: `https://sink.test.com/xyz${index}`,
});
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse("https://example1.com", 1),
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse("https://example2.com", 2),
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse("https://example3.com", 3),
});
const testSink = new SinkClient();
const results = await testSink.createMany([
"https://example1.com",
"https://example2.com",
"https://example3.com",
]);
expect(results).toEqual([
{ url: "https://example1.com", shortLink: "https://sink.test.com/xyz1" },
{ url: "https://example2.com", shortLink: "https://sink.test.com/xyz2" },
{ url: "https://example3.com", shortLink: "https://sink.test.com/xyz3" },
]);
expect(global.fetch).toHaveBeenCalledTimes(3);
});
it("should return original URLs when Sink is not configured", async () => {
vi.stubEnv("SINK_API_URL", "");
const testSink = new SinkClient();
const results = await testSink.createMany(["https://example1.com", "https://example2.com"]);
expect(results).toEqual([
{ url: "https://example1.com", shortLink: null },
{ url: "https://example2.com", shortLink: null },
]);
expect(global.fetch).not.toHaveBeenCalled();
});
it("should handle empty URLs in the array", async () => {
const mockResponse = {
link: {
id: "abc123",
url: "https://example.com",
slug: "xyz",
createdAt: "2024-01-01",
updatedAt: "2024-01-01",
views: 0,
},
shortLink: "https://sink.test.com/xyz",
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const testSink = new SinkClient();
const results = await testSink.createMany(["", "https://example.com", ""]);
expect(results[0]).toEqual({ url: "", shortLink: null });
expect(results[1]).toEqual({ url: "https://example.com", shortLink: "https://sink.test.com/xyz" });
expect(results[2]).toEqual({ url: "", shortLink: null });
expect(global.fetch).toHaveBeenCalledTimes(1);
});
it("should handle partial failures gracefully", async () => {
const mockSuccessResponse = {
link: {
id: "abc1",
url: "https://example1.com",
slug: "xyz1",
createdAt: "2024-01-01",
updatedAt: "2024-01-01",
views: 0,
},
shortLink: "https://sink.test.com/xyz1",
};
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => mockSuccessResponse,
})
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValueOnce({
ok: false,
status: 500,
text: async () => "Server error",
});
const testSink = new SinkClient();
const results = await testSink.createMany([
"https://example1.com",
"https://example2.com",
"https://example3.com",
]);
expect(results).toEqual([
{ url: "https://example1.com", shortLink: "https://sink.test.com/xyz1" },
{ url: "https://example2.com", shortLink: null },
{ url: "https://example3.com", shortLink: null },
]);
});
it("should handle empty array", async () => {
const testSink = new SinkClient();
const results = await testSink.createMany([]);
expect(results).toEqual([]);
expect(global.fetch).not.toHaveBeenCalled();
});
it("should process all URLs in parallel", async () => {
const mockResponse = {
link: {
id: "abc",
url: "https://example.com",
slug: "xyz",
createdAt: "2024-01-01",
updatedAt: "2024-01-01",
views: 0,
},
shortLink: "https://sink.test.com/xyz",
};
mockFetch.mockResolvedValue({
ok: true,
json: async () => mockResponse,
});
const testSink = new SinkClient();
const urls = Array(10).fill("https://example.com");
await testSink.createMany(urls);
expect(global.fetch).toHaveBeenCalledTimes(10);
});
});
});