xtablo-source/api/src/__tests__/helpers.test.ts
2025-10-11 12:32:52 +02:00

426 lines
13 KiB
TypeScript

import { expect } from "chai";
import { afterEach, beforeEach, describe, it } from "mocha";
import sinon from "sinon";
import {
generateICSFromEvents,
getTabloFileNames,
isTabloAdmin,
isTabloMember,
writeCalendarFileToR2,
} from "../helpers.js";
import type { EventAndTablo } from "../types.js";
import {
createMockS3Client,
createMockSupabaseClient,
mockEnvVars,
mockEvent,
mockTablo,
mockUser,
} from "./test-utils.js";
describe("Helper Functions", () => {
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
let mockSupabase: any;
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
let mockS3: any;
let restoreEnv: () => void;
beforeEach(() => {
restoreEnv = mockEnvVars();
mockSupabase = createMockSupabaseClient();
mockS3 = createMockS3Client();
});
afterEach(() => {
sinon.restore();
restoreEnv();
});
describe("generateICSFromEvents", () => {
it("should generate valid ICS content from events", () => {
const events: EventAndTablo[] = [
{
event_id: "event1",
tablo_id: "tablo1",
tablo_name: "Test Tablo",
tablo_color: "bg-blue-500",
tablo_status: "todo",
title: "Test Event",
description: "Test description",
start_date: "2024-01-16",
start_time: "10:00:00",
end_time: "11:00:00",
// created_by: mockUser.id,
// created_at: "2024-01-01T00:00:00Z",
// deleted_at: null,
},
];
const icsContent = generateICSFromEvents(events, "Test Calendar");
expect(icsContent).to.include("BEGIN:VCALENDAR");
expect(icsContent).to.include("VERSION:2.0");
expect(icsContent).to.include("X-WR-CALNAME:Test Calendar");
expect(icsContent).to.include("BEGIN:VEVENT");
expect(icsContent).to.include("SUMMARY:Test Event");
expect(icsContent).to.include("DESCRIPTION:Tablo: Test Tablo");
expect(icsContent).to.include("END:VEVENT");
expect(icsContent).to.include("END:VCALENDAR");
});
it("should handle events without end_time", () => {
const events: EventAndTablo[] = [
{
event_id: "event1",
tablo_id: "tablo1",
tablo_name: "Test Tablo",
tablo_color: "bg-blue-500",
tablo_status: "todo",
title: "Test Event",
description: null,
start_date: "2024-01-16",
start_time: "10:00:00",
end_time: null,
created_by: mockUser.id,
created_at: "2024-01-01T00:00:00Z",
deleted_at: null,
// biome-ignore lint/suspicious/noExplicitAny: Mock event with null end_time
} as any,
];
const icsContent = generateICSFromEvents(events, "Test Calendar");
expect(icsContent).to.include("BEGIN:VEVENT");
expect(icsContent).to.include("SUMMARY:Test Event");
expect(icsContent).to.include("END:VEVENT");
});
it("should escape special characters in ICS text", () => {
const events: EventAndTablo[] = [
{
event_id: "event1",
tablo_id: "tablo1",
tablo_name: "Test; Tablo,",
tablo_color: "bg-blue-500",
tablo_status: "todo",
title: "Test; Event,",
description: "Test\\description\nwith newline",
start_date: "2024-01-16",
start_time: "10:00:00",
end_time: "11:00:00",
// created_by: mockUser.id,
// created_at: "2024-01-01T00:00:00Z",
// deleted_at: null,
},
];
const icsContent = generateICSFromEvents(events, "Test Calendar");
expect(icsContent).to.include("SUMMARY:Test\\; Event\\,");
expect(icsContent).to.include(
"DESCRIPTION:Tablo: Test\\; Tablo\\,\\nTest\\\\description\\nwith newline"
);
});
it("should skip events without required fields", () => {
const events: EventAndTablo[] = [
{
event_id: "event1",
tablo_id: "tablo1",
tablo_name: "Test Tablo",
tablo_color: "bg-blue-500",
tablo_status: "todo",
// biome-ignore lint/suspicious/noExplicitAny: Testing null title case
title: null as any,
description: null,
start_date: "2024-01-16",
start_time: "10:00:00",
end_time: "11:00:00",
// created_by: mockUser.id,
// created_at: "2024-01-01T00:00:00Z",
// deleted_at: null,
},
];
const icsContent = generateICSFromEvents(events, "Test Calendar");
expect(icsContent).to.include("BEGIN:VCALENDAR");
expect(icsContent).to.not.include("BEGIN:VEVENT");
expect(icsContent).to.include("END:VCALENDAR");
});
it("should handle multiple events", () => {
const events: EventAndTablo[] = [
{
event_id: "event1",
tablo_id: "tablo1",
tablo_name: "Test Tablo",
tablo_color: "bg-blue-500",
tablo_status: "todo",
title: "Event 1",
description: "Description 1",
start_date: "2024-01-16",
start_time: "10:00:00",
end_time: "11:00:00",
// created_by: mockUser.id,
// created_at: "2024-01-01T00:00:00Z",
// deleted_at: null,
},
{
event_id: "event2",
tablo_id: "tablo1",
tablo_name: "Test Tablo",
tablo_color: "bg-blue-500",
tablo_status: "todo",
title: "Event 2",
description: "Description 2",
start_date: "2024-01-17",
start_time: "14:00:00",
end_time: "15:00:00",
// created_by: mockUser.id,
// created_at: "2024-01-01T00:00:00Z",
// deleted_at: null,
},
];
const icsContent = generateICSFromEvents(events, "Test Calendar");
const eventCount = (icsContent.match(/BEGIN:VEVENT/g) || []).length;
expect(eventCount).to.equal(2);
expect(icsContent).to.include("SUMMARY:Event 1");
expect(icsContent).to.include("SUMMARY:Event 2");
});
});
describe("writeCalendarFileToR2", () => {
it("should write calendar file to R2 successfully", async () => {
const events: EventAndTablo[] = [
{
event_id: "event1",
tablo_id: mockTablo.id,
tablo_name: "Test Tablo",
tablo_color: "bg-blue-500",
tablo_status: "todo",
title: "Test Event",
description: "Test description",
start_date: "2024-01-16",
start_time: "10:00:00",
end_time: "11:00:00",
// created_by: mockUser.id,
// created_at: "2024-01-01T00:00:00Z",
// deleted_at: null,
},
];
const eventsBuilder = {
select: sinon.stub().returnsThis(),
eq: sinon.stub().resolves({ data: events, error: null }),
};
mockSupabase.from.withArgs("events_and_tablos").returns(eventsBuilder);
mockS3.send.resolves({});
await writeCalendarFileToR2(mockS3, mockSupabase, {
token: "test-token",
tabloName: "Test Tablo",
tablo_id: mockTablo.id,
});
expect(mockS3.send.calledOnce).to.be.true;
});
it("should throw error if events fetch fails", async () => {
const eventsBuilder = {
select: sinon.stub().returnsThis(),
eq: sinon
.stub()
.resolves({ data: null, error: { message: "Database error" } }),
};
mockSupabase.from.withArgs("events_and_tablos").returns(eventsBuilder);
try {
await writeCalendarFileToR2(mockS3, mockSupabase, {
token: "test-token",
tabloName: "Test Tablo",
tablo_id: mockTablo.id,
});
expect.fail("Should have thrown an error");
// biome-ignore lint/suspicious/noExplicitAny: Catching error to check message
} catch (error: any) {
expect(error.message).to.equal("Failed to generate events");
}
});
});
describe("isTabloMember", () => {
it("should return true if user is a member", async () => {
const accessBuilder = {
select: sinon.stub().returnsThis(),
eq: sinon.stub().returnsThis(),
};
// The last eq() call should resolve with data
accessBuilder.eq.onCall(2).resolves({
data: [{ tablo_id: mockTablo.id, user_id: mockUser.id }],
error: null,
});
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
const isMember = await isTabloMember(
mockSupabase,
mockTablo.id,
mockUser.id
);
expect(isMember).to.be.true;
});
it("should return false if user is not a member", async () => {
const accessBuilder = {
select: sinon.stub().returnsThis(),
eq: sinon.stub().returnsThis(),
};
// The last eq() call should resolve with empty data
accessBuilder.eq.onCall(2).resolves({ data: [], error: null });
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
const isMember = await isTabloMember(
mockSupabase,
mockTablo.id,
mockUser.id
);
expect(isMember).to.be.false;
});
it("should return false if database error occurs", async () => {
const accessBuilder = {
select: sinon.stub().returnsThis(),
eq: sinon.stub().returnsThis(),
};
// The last eq() call should resolve with error
accessBuilder.eq
.onCall(2)
.resolves({ data: null, error: { message: "Database error" } });
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
const isMember = await isTabloMember(
mockSupabase,
mockTablo.id,
mockUser.id
);
expect(isMember).to.be.false;
});
});
describe("isTabloAdmin", () => {
it("should return true if user is an admin", async () => {
const accessBuilder = {
select: sinon.stub().returnsThis(),
eq: sinon.stub().returnsThis(),
};
// The last eq() call (4th call - onCall(3)) should resolve with data
accessBuilder.eq.onCall(3).resolves({
data: [
{ tablo_id: mockTablo.id, user_id: mockUser.id, is_admin: true },
],
error: null,
});
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
const isAdmin = await isTabloAdmin(
mockSupabase,
mockTablo.id,
mockUser.id
);
expect(isAdmin).to.be.true;
});
it("should return false if user is not an admin", async () => {
const accessBuilder = {
select: sinon.stub().returnsThis(),
eq: sinon.stub().returnsThis(),
};
// The last eq() call should resolve with empty data
accessBuilder.eq.onCall(3).resolves({ data: [], error: null });
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
const isAdmin = await isTabloAdmin(
mockSupabase,
mockTablo.id,
mockUser.id
);
expect(isAdmin).to.be.false;
});
it("should return false if database error occurs", async () => {
const accessBuilder = {
select: sinon.stub().returnsThis(),
eq: sinon.stub().returnsThis(),
};
// The last eq() call should resolve with error
accessBuilder.eq
.onCall(3)
.resolves({ data: null, error: { message: "Database error" } });
mockSupabase.from.withArgs("tablo_access").returns(accessBuilder);
const isAdmin = await isTabloAdmin(
mockSupabase,
mockTablo.id,
mockUser.id
);
expect(isAdmin).to.be.false;
});
});
describe("getTabloFileNames", () => {
it("should return list of file names", async () => {
mockS3.send.resolves({
Contents: [
{ Key: `${mockTablo.id}/file1.txt` },
{ Key: `${mockTablo.id}/file2.pdf` },
{ Key: `${mockTablo.id}/file3.jpg` },
],
});
const fileNames = await getTabloFileNames(mockS3, mockTablo.id);
expect(fileNames).to.deep.equal(["file1.txt", "file2.pdf", "file3.jpg"]);
});
it("should return empty array if no files exist", async () => {
mockS3.send.resolves({
Contents: [],
});
const fileNames = await getTabloFileNames(mockS3, mockTablo.id);
expect(fileNames).to.deep.equal([]);
});
it("should filter out invalid file names", async () => {
mockS3.send.resolves({
Contents: [
{ Key: `${mockTablo.id}/file1.txt` },
{ Key: `${mockTablo.id}/` }, // Empty file name
{ Key: `${mockTablo.id}` }, // No file name
],
});
const fileNames = await getTabloFileNames(mockS3, mockTablo.id);
expect(fileNames).to.deep.equal(["file1.txt"]);
});
});
});