427 lines
13 KiB
TypeScript
427 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"]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|