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"]); }); }); });