* 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>
300 lines
8.4 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|