#708 apply strictier linting rules (#717)

* #708 apply strictier linting rules and fix simple eslint bugs

* #708 fix eslint errors relate to promise

* #708 fix eslint import/no-extraneous-dependencies

* #708 fix eslint errors of react-hook

* #708 enable eslint check for typescript

---------

Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
lethemanh
2026-04-01 22:15:10 +07:00
committed by GitHub
parent 2bff6aae78
commit cadfa70e60
321 changed files with 27452 additions and 27600 deletions
+51 -51
View File
@@ -2,72 +2,72 @@ import {
calendarRangeManager,
getDisplayedCalendarRange,
getDisplayedDate,
setDisplayedDateAndRange,
} from "@/utils/CalendarRangeManager";
setDisplayedDateAndRange
} from '@/utils/CalendarRangeManager'
describe("CalendarRangeManager", () => {
describe('CalendarRangeManager', () => {
beforeEach(() => {
setDisplayedDateAndRange(new Date());
});
setDisplayedDateAndRange(new Date())
})
it("should return a singleton instance", () => {
const instance1 = calendarRangeManager;
const instance2 = calendarRangeManager;
it('should return a singleton instance', () => {
const instance1 = calendarRangeManager
const instance2 = calendarRangeManager
expect(instance1).toStrictEqual(instance2);
});
expect(instance1).toStrictEqual(instance2)
})
it("should compute and return calendar range when date is set", () => {
const testDate = new Date("2025-06-15T10:00:00Z");
setDisplayedDateAndRange(testDate);
it('should compute and return calendar range when date is set', () => {
const testDate = new Date('2025-06-15T10:00:00Z')
setDisplayedDateAndRange(testDate)
const range = getDisplayedCalendarRange();
expect(range.start).toBeInstanceOf(Date);
expect(range.end).toBeInstanceOf(Date);
expect(range.start.getTime()).toBeLessThanOrEqual(testDate.getTime());
expect(range.end.getTime()).toBeGreaterThanOrEqual(testDate.getTime());
});
const range = getDisplayedCalendarRange()
expect(range.start).toBeInstanceOf(Date)
expect(range.end).toBeInstanceOf(Date)
expect(range.start.getTime()).toBeLessThanOrEqual(testDate.getTime())
expect(range.end.getTime()).toBeGreaterThanOrEqual(testDate.getTime())
})
it("should get the default date", () => {
const date = getDisplayedDate();
expect(date).toBeInstanceOf(Date);
});
it('should get the default date', () => {
const date = getDisplayedDate()
expect(date).toBeInstanceOf(Date)
})
it("should set and get a date", () => {
const testDate = new Date("2025-01-15T10:00:00Z");
setDisplayedDateAndRange(testDate);
it('should set and get a date', () => {
const testDate = new Date('2025-01-15T10:00:00Z')
setDisplayedDateAndRange(testDate)
const retrievedDate = getDisplayedDate();
expect(retrievedDate).toStrictEqual(testDate);
});
const retrievedDate = getDisplayedDate()
expect(retrievedDate).toStrictEqual(testDate)
})
it("should persist date across multiple calls", () => {
const testDate = new Date("2025-06-20T15:30:00Z");
setDisplayedDateAndRange(testDate);
it('should persist date across multiple calls', () => {
const testDate = new Date('2025-06-20T15:30:00Z')
setDisplayedDateAndRange(testDate)
const date1 = getDisplayedDate();
const date2 = getDisplayedDate();
const date1 = getDisplayedDate()
const date2 = getDisplayedDate()
expect(date1).toStrictEqual(date2);
expect(date1).toStrictEqual(testDate);
});
expect(date1).toStrictEqual(date2)
expect(date1).toStrictEqual(testDate)
})
it("should update date when set multiple times", () => {
const date1 = new Date("2025-01-01T00:00:00Z");
const date2 = new Date("2025-12-31T23:59:59Z");
it('should update date when set multiple times', () => {
const date1 = new Date('2025-01-01T00:00:00Z')
const date2 = new Date('2025-12-31T23:59:59Z')
setDisplayedDateAndRange(date1);
expect(getDisplayedDate()).toStrictEqual(date1);
setDisplayedDateAndRange(date1)
expect(getDisplayedDate()).toStrictEqual(date1)
setDisplayedDateAndRange(date2);
expect(getDisplayedDate()).toStrictEqual(date2);
});
setDisplayedDateAndRange(date2)
expect(getDisplayedDate()).toStrictEqual(date2)
})
it("should maintain shared state (singleton behavior)", () => {
const testDate = new Date("2025-03-01");
setDisplayedDateAndRange(testDate);
it('should maintain shared state (singleton behavior)', () => {
const testDate = new Date('2025-03-01')
setDisplayedDateAndRange(testDate)
// Verify CalendarRangeManager reflects the same state
expect(calendarRangeManager.getDate()).toStrictEqual(testDate);
});
});
expect(calendarRangeManager.getDate()).toStrictEqual(testDate)
})
})
+24 -24
View File
@@ -1,15 +1,15 @@
import type { AppStore, RootState } from "@/app/store";
import { setupStore } from "@/app/store";
import { TwakeMuiThemeProvider } from "@linagora/twake-mui";
import type { RenderOptions } from "@testing-library/react";
import { render } from "@testing-library/react";
import React, { PropsWithChildren } from "react";
import { Provider } from "react-redux";
import { MemoryRouter } from "react-router-dom";
import { I18nContext } from "twake-i18n";
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
preloadedState?: Partial<RootState>;
store?: AppStore;
import type { AppStore, RootState } from '@/app/store'
import { setupStore } from '@/app/store'
import { TwakeMuiThemeProvider } from '@linagora/twake-mui'
import type { RenderOptions } from '@testing-library/react'
import { render } from '@testing-library/react'
import React, { PropsWithChildren } from 'react'
import { Provider } from 'react-redux'
import { MemoryRouter } from 'react-router-dom'
import { I18nContext } from 'twake-i18n'
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
preloadedState?: Partial<RootState>
store?: AppStore
}
export function renderWithProviders(
@@ -18,7 +18,7 @@ export function renderWithProviders(
extendedRenderOptions: ExtendedRenderOptions = {}
) {
const { store = setupStore(preloadedState), ...renderOptions } =
extendedRenderOptions;
extendedRenderOptions
const Wrapper = ({ children }: PropsWithChildren) => {
return (
@@ -26,31 +26,31 @@ export function renderWithProviders(
<I18nContext.Provider
value={{
t: (key: string, vars?: Record<string, string>) => {
if (key === "locale") {
return "en";
if (key === 'locale') {
return 'en'
}
if (vars) {
const params = Object.entries(vars)
.map(([k, v]) => `${k}=${v}`)
.join(",");
return `${key}(${params})`;
.join(',')
return `${key}(${params})`
}
return key;
return key
},
f: (date: Date, formatStr: string) => date.toString(),
lang: "en",
lang: 'en'
}}
>
<MemoryRouter initialEntries={["/"]}>
<MemoryRouter initialEntries={['/']}>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</I18nContext.Provider>
</TwakeMuiThemeProvider>
);
};
)
}
return {
store,
...render(ui, { wrapper: Wrapper, ...renderOptions }),
};
...render(ui, { wrapper: Wrapper, ...renderOptions })
}
}
+93 -93
View File
@@ -1,113 +1,113 @@
import { getInitials, stringToGradient } from "@/utils/avatarUtils";
import { getInitials, stringToGradient } from '@/utils/avatarUtils'
jest.mock("@linagora/twake-mui", () => ({
jest.mock('@linagora/twake-mui', () => ({
nameToColor: jest.fn((name: string) => {
if (!name) return undefined;
if (!name) return undefined
const colors = [
"sunrise",
"downy",
"sugarCoral",
"pinkBonnet",
"blueMana",
"nightBlue",
"snowPea",
"pluviophile",
"cornflower",
"paleGreen",
"moonBlue",
];
'sunrise',
'downy',
'sugarCoral',
'pinkBonnet',
'blueMana',
'nightBlue',
'snowPea',
'pluviophile',
'cornflower',
'paleGreen',
'moonBlue'
]
const hash = Array.from(name.toUpperCase())
.map((letter) => letter.charCodeAt(0))
.reduce((sum, number) => sum + number, 0);
return colors[hash % colors.length];
}),
}));
.map(letter => letter.charCodeAt(0))
.reduce((sum, number) => sum + number, 0)
return colors[hash % colors.length]
})
}))
describe("avatarUtils", () => {
describe("getInitials", () => {
it("returns 2-letter initials for full name", () => {
expect(getInitials("John Doe")).toBe("JD");
expect(getInitials("Alice Smith")).toBe("AS");
expect(getInitials("Nguyễn Văn")).toBe("NV");
});
describe('avatarUtils', () => {
describe('getInitials', () => {
it('returns 2-letter initials for full name', () => {
expect(getInitials('John Doe')).toBe('JD')
expect(getInitials('Alice Smith')).toBe('AS')
expect(getInitials('Nguyễn Văn')).toBe('NV')
})
it("returns single letter for single word", () => {
expect(getInitials("Alice")).toBe("A");
expect(getInitials("John")).toBe("J");
});
it('returns single letter for single word', () => {
expect(getInitials('Alice')).toBe('A')
expect(getInitials('John')).toBe('J')
})
it("returns uppercase initials", () => {
expect(getInitials("john doe")).toBe("JD");
expect(getInitials("ALICE SMITH")).toBe("AS");
expect(getInitials("a")).toBe("A");
});
it('returns uppercase initials', () => {
expect(getInitials('john doe')).toBe('JD')
expect(getInitials('ALICE SMITH')).toBe('AS')
expect(getInitials('a')).toBe('A')
})
it("handles multiple spaces between words", () => {
expect(getInitials("John Doe")).toBe("JD");
expect(getInitials("Alice Smith Brown")).toBe("AS");
});
it('handles multiple spaces between words', () => {
expect(getInitials('John Doe')).toBe('JD')
expect(getInitials('Alice Smith Brown')).toBe('AS')
})
it("handles email addresses", () => {
expect(getInitials("john.doe@email.com")).toBe("J");
expect(getInitials("test@example.com")).toBe("T");
});
it('handles email addresses', () => {
expect(getInitials('john.doe@email.com')).toBe('J')
expect(getInitials('test@example.com')).toBe('T')
})
it("handles empty string", () => {
expect(getInitials("")).toBe("");
});
it('handles empty string', () => {
expect(getInitials('')).toBe('')
})
it("handles whitespace-only string", () => {
expect(getInitials(" ")).toBe("");
});
it('handles whitespace-only string', () => {
expect(getInitials(' ')).toBe('')
})
it("handles single character", () => {
expect(getInitials("A")).toBe("A");
expect(getInitials("a")).toBe("A");
});
it('handles single character', () => {
expect(getInitials('A')).toBe('A')
expect(getInitials('a')).toBe('A')
})
it("handles names with special characters", () => {
expect(getInitials("Jean-Pierre")).toBe("J");
expect(getInitials("Mary Jane Watson")).toBe("MJ");
});
it('handles names with special characters', () => {
expect(getInitials('Jean-Pierre')).toBe('J')
expect(getInitials('Mary Jane Watson')).toBe('MJ')
})
it("handles unicode characters", () => {
expect(getInitials("Nguyễn Văn")).toBe("NV");
expect(getInitials("José María")).toBe("JM");
});
it('handles unicode characters', () => {
expect(getInitials('Nguyễn Văn')).toBe('NV')
expect(getInitials('José María')).toBe('JM')
})
it("takes first two words for names with more than two words", () => {
expect(getInitials("John Michael Smith")).toBe("JM");
expect(getInitials("Alice Bob Charlie")).toBe("AB");
});
});
it('takes first two words for names with more than two words', () => {
expect(getInitials('John Michael Smith')).toBe('JM')
expect(getInitials('Alice Bob Charlie')).toBe('AB')
})
})
describe("stringToGradient", () => {
it("returns color for valid string", () => {
const result = stringToGradient("John Doe");
expect(result).toBeDefined();
expect(typeof result).toBe("string");
});
describe('stringToGradient', () => {
it('returns color for valid string', () => {
const result = stringToGradient('John Doe')
expect(result).toBeDefined()
expect(typeof result).toBe('string')
})
it("returns same color for same input", () => {
const result1 = stringToGradient("John Doe");
const result2 = stringToGradient("John Doe");
expect(result1).toBe(result2);
});
it('returns same color for same input', () => {
const result1 = stringToGradient('John Doe')
const result2 = stringToGradient('John Doe')
expect(result1).toBe(result2)
})
it("returns undefined for empty string", () => {
expect(stringToGradient("")).toBeUndefined();
});
it('returns undefined for empty string', () => {
expect(stringToGradient('')).toBeUndefined()
})
it("returns color for single character", () => {
const result = stringToGradient("A");
expect(result).toBeDefined();
expect(typeof result).toBe("string");
});
it('returns color for single character', () => {
const result = stringToGradient('A')
expect(result).toBeDefined()
expect(typeof result).toBe('string')
})
it("returns color for email address", () => {
const result = stringToGradient("test@example.com");
expect(result).toBeDefined();
expect(typeof result).toBe("string");
});
});
});
it('returns color for email address', () => {
const result = stringToGradient('test@example.com')
expect(result).toBeDefined()
expect(typeof result).toBe('string')
})
})
})
+36 -36
View File
@@ -1,51 +1,51 @@
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { buildDelegatedEventURL } from "@/features/Events/utils";
import { Calendar } from '@/features/Calendars/CalendarTypes'
import { buildDelegatedEventURL } from '@/features/Events/utils'
const makeCalendar = (link: string): Calendar =>
({
id: "user2/cal1",
id: 'user2/cal1',
delegated: true,
link,
owner: { emails: ["owner@example.com"] },
}) as Calendar;
owner: { emails: ['owner@example.com'] }
}) as Calendar
describe("buildDelegatedEventURL", () => {
it("rebases event filename onto calendar link base path", () => {
const calendar = makeCalendar("/calendars/user2/cal1.json");
const eventURL = "/calendars/someother/path/event-abc.ics";
describe('buildDelegatedEventURL', () => {
it('rebases event filename onto calendar link base path', () => {
const calendar = makeCalendar('/calendars/user2/cal1.json')
const eventURL = '/calendars/someother/path/event-abc.ics'
expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
"/calendars/user2/cal1/event-abc.ics"
);
});
'/calendars/user2/cal1/event-abc.ics'
)
})
it("strips .json suffix from calendar link", () => {
const calendar = makeCalendar("/calendars/user2/cal1.json");
const eventURL = "/calendars/user2/cal1/event-abc.ics";
const result = buildDelegatedEventURL(calendar, eventURL);
expect(result).not.toContain(".json");
});
it('strips .json suffix from calendar link', () => {
const calendar = makeCalendar('/calendars/user2/cal1.json')
const eventURL = '/calendars/user2/cal1/event-abc.ics'
const result = buildDelegatedEventURL(calendar, eventURL)
expect(result).not.toContain('.json')
})
it("preserves the exact filename from the event URL", () => {
const calendar = makeCalendar("/calendars/user2/cal1.json");
const eventURL = "/calendars/user2/cal1/some-uid-with-dashes.ics";
it('preserves the exact filename from the event URL', () => {
const calendar = makeCalendar('/calendars/user2/cal1.json')
const eventURL = '/calendars/user2/cal1/some-uid-with-dashes.ics'
expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
"/calendars/user2/cal1/some-uid-with-dashes.ics"
);
});
'/calendars/user2/cal1/some-uid-with-dashes.ics'
)
})
it("throws when event URL has no filename", () => {
const calendar = makeCalendar("/calendars/user2/cal1.json");
const eventURL = "";
it('throws when event URL has no filename', () => {
const calendar = makeCalendar('/calendars/user2/cal1.json')
const eventURL = ''
expect(() => buildDelegatedEventURL(calendar, eventURL)).toThrow(
/Cannot extract filename from event URL/
);
});
)
})
it("works with nested calendar link paths", () => {
const calendar = makeCalendar("/dav/calendars/users/user2/cal1.json");
const eventURL = "/other/path/event-xyz.ics";
it('works with nested calendar link paths', () => {
const calendar = makeCalendar('/dav/calendars/users/user2/cal1.json')
const eventURL = '/other/path/event-xyz.ics'
expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
"/dav/calendars/users/user2/cal1/event-xyz.ics"
);
});
});
'/dav/calendars/users/user2/cal1/event-xyz.ics'
)
})
})
+140 -140
View File
@@ -1,231 +1,231 @@
import {
eventToFullCalendarFormat,
getCalendarVisibility,
} from "@/components/Calendar/utils/calendarUtils";
import { Calendar, DelegationAccess } from "@/features/Calendars/CalendarTypes";
import { AclEntry } from "@/features/Calendars/types/CalendarData";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { userOrganiser } from "@/features/User/userDataTypes";
getCalendarVisibility
} from '@/components/Calendar/utils/calendarUtils'
import { Calendar, DelegationAccess } from '@/features/Calendars/CalendarTypes'
import { AclEntry } from '@/features/Calendars/types/CalendarData'
import { CalendarEvent } from '@/features/Events/EventsTypes'
import { userOrganiser } from '@/features/User/userDataTypes'
describe("getCalendarVisibility", () => {
describe('getCalendarVisibility', () => {
it("returns 'public' when {DAV:}authenticated has {DAV:}read", () => {
const acl: AclEntry[] = [
{
privilege: "{DAV:}read",
principal: "{DAV:}authenticated",
protected: true,
},
];
expect(getCalendarVisibility(acl)).toBe("public");
});
privilege: '{DAV:}read',
principal: '{DAV:}authenticated',
protected: true
}
]
expect(getCalendarVisibility(acl)).toBe('public')
})
it("returns 'private' when {DAV:}authenticated only has read-free-busy", () => {
const acl: AclEntry[] = [
{
privilege: "{urn:ietf:params:xml:ns:caldav}read-free-busy",
principal: "{DAV:}authenticated",
protected: true,
},
];
expect(getCalendarVisibility(acl)).toBe("private");
});
privilege: '{urn:ietf:params:xml:ns:caldav}read-free-busy',
principal: '{DAV:}authenticated',
protected: true
}
]
expect(getCalendarVisibility(acl)).toBe('private')
})
it("returns 'private' when {DAV:}authenticated is not present", () => {
const acl: AclEntry[] = [
{
privilege: "{DAV:}read",
principal: "principals/users/123",
protected: true,
},
];
expect(getCalendarVisibility(acl)).toBe("private");
});
privilege: '{DAV:}read',
principal: 'principals/users/123',
protected: true
}
]
expect(getCalendarVisibility(acl)).toBe('private')
})
it("ignores non-authenticated principals and still returns 'private'", () => {
const acl: AclEntry[] = [
{
privilege: "{DAV:}read",
principal: "principals/users/other",
protected: true,
privilege: '{DAV:}read',
principal: 'principals/users/other',
protected: true
},
{
privilege: "{DAV:}write",
principal: "principals/users/other",
protected: true,
},
];
expect(getCalendarVisibility(acl)).toBe("private");
});
privilege: '{DAV:}write',
principal: 'principals/users/other',
protected: true
}
]
expect(getCalendarVisibility(acl)).toBe('private')
})
it("stops early when it finds a {DAV:}read for {DAV:}authenticated", () => {
it('stops early when it finds a {DAV:}read for {DAV:}authenticated', () => {
const acl: AclEntry[] = [
{
privilege: "{DAV:}read",
principal: "{DAV:}authenticated",
protected: true,
privilege: '{DAV:}read',
principal: '{DAV:}authenticated',
protected: true
},
{
privilege: "{DAV:}write",
principal: "{DAV:}authenticated",
protected: true,
},
];
expect(getCalendarVisibility(acl)).toBe("public");
});
});
privilege: '{DAV:}write',
principal: '{DAV:}authenticated',
protected: true
}
]
expect(getCalendarVisibility(acl)).toBe('public')
})
})
jest.mock("twake-i18n", () => ({
useI18n: () => ({ t: (key: string) => key }),
}));
jest.mock('twake-i18n', () => ({
useI18n: () => ({ t: (key: string) => key })
}))
const baseEvent: CalendarEvent = {
uid: "event-1",
calId: "user1/cal1",
title: "Test Event",
start: "2024-01-15T10:00:00",
end: "2024-01-15T11:00:00",
uid: 'event-1',
calId: 'user1/cal1',
title: 'Test Event',
start: '2024-01-15T10:00:00',
end: '2024-01-15T11:00:00',
allday: false,
organizer: { cal_address: "owner@example.com" } as userOrganiser,
color: { light: "#FF0000" },
class: "PUBLIC",
} as unknown as CalendarEvent;
organizer: { cal_address: 'owner@example.com' } as userOrganiser,
color: { light: '#FF0000' },
class: 'PUBLIC'
} as unknown as CalendarEvent
const makeCalendar = (overrides: Partial<Calendar> = {}): Calendar =>
({
id: "user1/cal1",
name: "Test Calendar",
id: 'user1/cal1',
name: 'Test Calendar',
delegated: false,
access: {} as DelegationAccess,
owner: { emails: ["alice@example.com"], firstname: "Alice" },
color: { light: "#FF0000" },
owner: { emails: ['alice@example.com'], firstname: 'Alice' },
color: { light: '#FF0000' },
events: {},
...overrides,
}) as Calendar;
...overrides
}) as Calendar
const callFormat = (
event: CalendarEvent,
calendars: Record<string, Calendar>,
userAddress = "alice@example.com",
userId = "user1"
userAddress = 'alice@example.com',
userId = 'user1'
) =>
eventToFullCalendarFormat([event], [], userId, userAddress, false, calendars);
eventToFullCalendarFormat([event], [], userId, userAddress, false, calendars)
describe("eventToFullCalendarFormat - editable flag", () => {
describe("personal calendar (non-delegated)", () => {
const calendar = makeCalendar({ delegated: false });
describe('eventToFullCalendarFormat - editable flag', () => {
describe('personal calendar (non-delegated)', () => {
const calendar = makeCalendar({ delegated: false })
it("is editable when user is organizer", () => {
it('is editable when user is organizer', () => {
const event = {
...baseEvent,
organizer: { cal_address: "alice@example.com" },
} as CalendarEvent;
const [result] = callFormat(event, { "user1/cal1": calendar });
expect(result.editable).toBe(true);
});
organizer: { cal_address: 'alice@example.com' }
} as CalendarEvent
const [result] = callFormat(event, { 'user1/cal1': calendar })
expect(result.editable).toBe(true)
})
it("is not editable when user is not organizer", () => {
it('is not editable when user is not organizer', () => {
const event = {
...baseEvent,
organizer: { cal_address: "other@example.com" },
} as CalendarEvent;
const [result] = callFormat(event, { "user1/cal1": calendar });
expect(result.editable).toBe(false);
});
organizer: { cal_address: 'other@example.com' }
} as CalendarEvent
const [result] = callFormat(event, { 'user1/cal1': calendar })
expect(result.editable).toBe(false)
})
it("is not editable when pending", () => {
it('is not editable when pending', () => {
const event = {
...baseEvent,
organizer: { cal_address: "alice@example.com" },
} as CalendarEvent;
organizer: { cal_address: 'alice@example.com' }
} as CalendarEvent
const [result] = eventToFullCalendarFormat(
[event],
[],
"user1",
"alice@example.com",
'user1',
'alice@example.com',
true,
{ "user1/cal1": calendar }
);
expect(result.editable).toBe(false);
});
});
{ 'user1/cal1': calendar }
)
expect(result.editable).toBe(false)
})
})
describe("delegated calendar", () => {
describe('delegated calendar', () => {
const writeDelegatedCalendar = makeCalendar({
id: "user2/cal1",
id: 'user2/cal1',
delegated: true,
access: { write: true } as DelegationAccess,
owner: { emails: ["owner@example.com"], firstname: "Owner" },
});
owner: { emails: ['owner@example.com'], firstname: 'Owner' }
})
const readDelegatedCalendar = makeCalendar({
id: "user2/cal1",
id: 'user2/cal1',
delegated: true,
access: { read: true } as DelegationAccess,
owner: { emails: ["owner@example.com"], firstname: "Owner" },
});
const event = { ...baseEvent, calId: "user2/cal1" };
owner: { emails: ['owner@example.com'], firstname: 'Owner' }
})
const event = { ...baseEvent, calId: 'user2/cal1' }
it("is editable when owner is organizer and delegated user has write rights", () => {
it('is editable when owner is organizer and delegated user has write rights', () => {
const [result] = callFormat(
{
...event,
organizer: { cal_address: "owner@example.com" },
organizer: { cal_address: 'owner@example.com' }
} as CalendarEvent,
{ "user2/cal1": writeDelegatedCalendar }
);
expect(result.editable).toBe(true);
});
{ 'user2/cal1': writeDelegatedCalendar }
)
expect(result.editable).toBe(true)
})
it("is not editable when owner is organizer but delegated user only has read access", () => {
it('is not editable when owner is organizer but delegated user only has read access', () => {
const [result] = callFormat(
{
...event,
organizer: { cal_address: "owner@example.com" },
organizer: { cal_address: 'owner@example.com' }
} as CalendarEvent,
{ "user2/cal1": readDelegatedCalendar }
);
expect(result.editable).toBe(false);
});
{ 'user2/cal1': readDelegatedCalendar }
)
expect(result.editable).toBe(false)
})
it("is not editable when owner is not organizer", () => {
it('is not editable when owner is not organizer', () => {
const [result] = callFormat(
{
...event,
organizer: { cal_address: "someone-else@example.com" },
organizer: { cal_address: 'someone-else@example.com' }
} as CalendarEvent,
{ "user2/cal1": writeDelegatedCalendar }
);
expect(result.editable).toBe(false);
});
{ 'user2/cal1': writeDelegatedCalendar }
)
expect(result.editable).toBe(false)
})
it("is not editable when pending even if owner is organizer", () => {
it('is not editable when pending even if owner is organizer', () => {
const [result] = eventToFullCalendarFormat(
[
{
...event,
organizer: { cal_address: "owner@example.com" },
} as CalendarEvent,
organizer: { cal_address: 'owner@example.com' }
} as CalendarEvent
],
[],
"user1",
"alice@example.com",
'user1',
'alice@example.com',
true,
{ "user2/cal1": writeDelegatedCalendar }
);
expect(result.editable).toBe(false);
});
{ 'user2/cal1': writeDelegatedCalendar }
)
expect(result.editable).toBe(false)
})
it("uses owner email not logged-in user email for organizer check", () => {
it('uses owner email not logged-in user email for organizer check', () => {
// logged-in user is alice, owner is owner@example.com
// organizer matches owner → editable
const [result] = callFormat(
{
...event,
organizer: { cal_address: "owner@example.com" },
organizer: { cal_address: 'owner@example.com' }
} as CalendarEvent,
{ "user2/cal1": writeDelegatedCalendar },
"alice@example.com" // logged-in user, should NOT be used for delegated
);
expect(result.editable).toBe(true);
});
});
});
{ 'user2/cal1': writeDelegatedCalendar },
'alice@example.com' // logged-in user, should NOT be used for delegated
)
expect(result.editable).toBe(true)
})
})
})
+88 -88
View File
@@ -1,120 +1,120 @@
import { createEventContext } from "@/features/Events/createEventContext";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { userData } from "@/features/User/userDataTypes";
import { userAttendee } from "@/features/User/models/attendee";
import { createEventContext } from '@/features/Events/createEventContext'
import { Calendar } from '@/features/Calendars/CalendarTypes'
import { CalendarEvent } from '@/features/Events/EventsTypes'
import { userData } from '@/features/User/userDataTypes'
import { userAttendee } from '@/features/User/models/attendee'
const makeUser = (email: string): userData => ({
email,
given_name: "Alice",
family_name: "User",
name: "Alice User",
sid: "user1",
sub: "user1",
});
given_name: 'Alice',
family_name: 'User',
name: 'Alice User',
sid: 'user1',
sub: 'user1'
})
const makeEvent = (overrides: Partial<CalendarEvent> = {}): CalendarEvent =>
({
uid: "event-1",
calId: "user1/cal1",
title: "Test Event",
start: "2024-01-15T10:00:00",
end: "2024-01-15T11:00:00",
organizer: { cal_address: "alice@example.com" },
uid: 'event-1',
calId: 'user1/cal1',
title: 'Test Event',
start: '2024-01-15T10:00:00',
end: '2024-01-15T11:00:00',
organizer: { cal_address: 'alice@example.com' },
attendee: [
{ cal_address: "alice@example.com", partstat: "ACCEPTED" },
{ cal_address: "owner@example.com", partstat: "ACCEPTED" },
{ cal_address: "other@example.com", partstat: "NEEDS-ACTION" },
{ cal_address: 'alice@example.com', partstat: 'ACCEPTED' },
{ cal_address: 'owner@example.com', partstat: 'ACCEPTED' },
{ cal_address: 'other@example.com', partstat: 'NEEDS-ACTION' }
],
...overrides,
}) as CalendarEvent;
...overrides
}) as CalendarEvent
const makeCalendar = (overrides: Partial<Calendar> = {}): Calendar =>
({
id: "user1/cal1",
name: "Test Calendar",
id: 'user1/cal1',
name: 'Test Calendar',
delegated: false,
owner: { emails: ["alice@example.com"], firstname: "Alice" },
owner: { emails: ['alice@example.com'], firstname: 'Alice' },
events: {},
...overrides,
}) as Calendar;
...overrides
}) as Calendar
describe("createEventContext", () => {
describe("non-delegated calendar", () => {
const calendar = makeCalendar({ delegated: false });
const user = makeUser("alice@example.com");
describe('createEventContext', () => {
describe('non-delegated calendar', () => {
const calendar = makeCalendar({ delegated: false })
const user = makeUser('alice@example.com')
it("finds currentUserAttendee by logged-in user email", () => {
const event = makeEvent();
const ctx = createEventContext(event, calendar, user);
expect(ctx.currentUserAttendee?.cal_address).toBe("alice@example.com");
});
it('finds currentUserAttendee by logged-in user email', () => {
const event = makeEvent()
const ctx = createEventContext(event, calendar, user)
expect(ctx.currentUserAttendee?.cal_address).toBe('alice@example.com')
})
it("returns null currentUserAttendee when user is not an attendee", () => {
const event = makeEvent({ attendee: [] });
const ctx = createEventContext(event, calendar, user);
expect(ctx.currentUserAttendee).toBeUndefined();
});
it('returns null currentUserAttendee when user is not an attendee', () => {
const event = makeEvent({ attendee: [] })
const ctx = createEventContext(event, calendar, user)
expect(ctx.currentUserAttendee).toBeUndefined()
})
it("isOwn is true when user email is in owner emails", () => {
const ctx = createEventContext(makeEvent(), calendar, user);
expect(ctx.isOwn).toBe(true);
});
it('isOwn is true when user email is in owner emails', () => {
const ctx = createEventContext(makeEvent(), calendar, user)
expect(ctx.isOwn).toBe(true)
})
it("isOwn is false when user email is not in owner emails", () => {
const other = makeUser("other@example.com");
const ctx = createEventContext(makeEvent(), calendar, other);
expect(ctx.isOwn).toBe(false);
});
});
it('isOwn is false when user email is not in owner emails', () => {
const other = makeUser('other@example.com')
const ctx = createEventContext(makeEvent(), calendar, other)
expect(ctx.isOwn).toBe(false)
})
})
describe("delegated calendar", () => {
describe('delegated calendar', () => {
const calendar = makeCalendar({
id: "user2/cal1",
id: 'user2/cal1',
delegated: true,
owner: { emails: ["owner@example.com"], firstname: "Owner" },
});
const user = makeUser("alice@example.com"); // logged-in user, not the owner
owner: { emails: ['owner@example.com'], firstname: 'Owner' }
})
const user = makeUser('alice@example.com') // logged-in user, not the owner
it("finds currentUserAttendee by owner email, not logged-in user email", () => {
const event = makeEvent({ calId: "user2/cal1" });
const ctx = createEventContext(event, calendar, user);
expect(ctx.currentUserAttendee?.cal_address).toBe("owner@example.com");
});
it('finds currentUserAttendee by owner email, not logged-in user email', () => {
const event = makeEvent({ calId: 'user2/cal1' })
const ctx = createEventContext(event, calendar, user)
expect(ctx.currentUserAttendee?.cal_address).toBe('owner@example.com')
})
it("does not find logged-in user as attendee for delegated calendar", () => {
it('does not find logged-in user as attendee for delegated calendar', () => {
const event = makeEvent({
calId: "user2/cal1",
calId: 'user2/cal1',
attendee: [
{
cal_address: "owner@example.com",
partstat: "ACCEPTED",
} as userAttendee,
],
});
const ctx = createEventContext(event, calendar, user);
cal_address: 'owner@example.com',
partstat: 'ACCEPTED'
} as userAttendee
]
})
const ctx = createEventContext(event, calendar, user)
// currentUserAttendee should be owner's, not alice's
expect(ctx.currentUserAttendee?.cal_address).toBe("owner@example.com");
});
expect(ctx.currentUserAttendee?.cal_address).toBe('owner@example.com')
})
it("returns undefined currentUserAttendee when owner is not an attendee", () => {
it('returns undefined currentUserAttendee when owner is not an attendee', () => {
const event = makeEvent({
calId: "user2/cal1",
calId: 'user2/cal1',
attendee: [
{
cal_address: "alice@example.com",
partstat: "ACCEPTED",
} as userAttendee,
],
});
const ctx = createEventContext(event, calendar, user);
expect(ctx.currentUserAttendee).toBeUndefined();
});
cal_address: 'alice@example.com',
partstat: 'ACCEPTED'
} as userAttendee
]
})
const ctx = createEventContext(event, calendar, user)
expect(ctx.currentUserAttendee).toBeUndefined()
})
it("passes event through unchanged", () => {
const event = makeEvent({ calId: "user2/cal1" });
const ctx = createEventContext(event, calendar, user);
expect(ctx.event).toBe(event);
});
});
});
it('passes event through unchanged', () => {
const event = makeEvent({ calId: 'user2/cal1' })
const ctx = createEventContext(event, calendar, user)
expect(ctx.event).toBe(event)
})
})
})
+30 -30
View File
@@ -1,38 +1,38 @@
import { getCalendarRange } from "@/utils/dateUtils";
import { getCalendarRange } from '@/utils/dateUtils'
describe("getCalendarRange", () => {
it("Nov 2025 (5 weeks): 2025-10-27 to 2025-11-30", () => {
describe('getCalendarRange', () => {
it('Nov 2025 (5 weeks): 2025-10-27 to 2025-11-30', () => {
const { start, end } = getCalendarRange(
new Date("2025-11-01T00:00:00.000Z")
);
expect(start.toISOString().slice(0, 10)).toBe("2025-10-27");
expect(end.toISOString().slice(0, 10)).toBe("2025-11-30");
});
new Date('2025-11-01T00:00:00.000Z')
)
expect(start.toISOString().slice(0, 10)).toBe('2025-10-27')
expect(end.toISOString().slice(0, 10)).toBe('2025-11-30')
})
it("Dec 2025 (6 weeks): end Sunday 2026-01-04", () => {
it('Dec 2025 (6 weeks): end Sunday 2026-01-04', () => {
const { start, end } = getCalendarRange(
new Date("2025-12-01T00:00:00.000Z")
);
expect(start.toISOString().slice(0, 10)).toBe("2025-12-01");
expect(end.toISOString().slice(0, 10)).toBe("2026-01-04");
});
new Date('2025-12-01T00:00:00.000Z')
)
expect(start.toISOString().slice(0, 10)).toBe('2025-12-01')
expect(end.toISOString().slice(0, 10)).toBe('2026-01-04')
})
it("Feb 2025 (short month): end Sunday 2025-03-02", () => {
it('Feb 2025 (short month): end Sunday 2025-03-02', () => {
const { start, end } = getCalendarRange(
new Date("2025-02-01T00:00:00.000Z")
);
expect(start.toISOString().slice(0, 10)).toBe("2025-01-27");
expect(end.toISOString().slice(0, 10)).toBe("2025-03-02");
});
new Date('2025-02-01T00:00:00.000Z')
)
expect(start.toISOString().slice(0, 10)).toBe('2025-01-27')
expect(end.toISOString().slice(0, 10)).toBe('2025-03-02')
})
it("sets boundary times correctly (start 00:00, end 23:59)", () => {
it('sets boundary times correctly (start 00:00, end 23:59)', () => {
const { start, end } = getCalendarRange(
new Date("2025-11-01T00:00:00.000Z")
);
expect(start.getHours()).toBe(0);
expect(start.getMinutes()).toBe(0);
expect(start.getSeconds()).toBe(0);
expect(end.getHours()).toBe(23);
expect(end.getMinutes()).toBe(59);
});
});
new Date('2025-11-01T00:00:00.000Z')
)
expect(start.getHours()).toBe(0)
expect(start.getMinutes()).toBe(0)
expect(start.getSeconds()).toBe(0)
expect(end.getHours()).toBe(23)
expect(end.getMinutes()).toBe(59)
})
})
+62 -62
View File
@@ -1,99 +1,99 @@
import { RootState } from "@/app/store";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { findCalendarById } from "@/utils";
import { RootState } from '@/app/store'
import { Calendar } from '@/features/Calendars/CalendarTypes'
import { findCalendarById } from '@/utils'
describe("findCalendarById", () => {
describe('findCalendarById', () => {
const mockCalendar1: Calendar = {
id: "cal1",
name: "Personal",
color: { light: "#FF0000" },
} as unknown as Calendar;
id: 'cal1',
name: 'Personal',
color: { light: '#FF0000' }
} as unknown as Calendar
const mockCalendar2: Calendar = {
id: "cal2",
name: "Work",
color: { light: "#0000FF" },
} as unknown as Calendar;
id: 'cal2',
name: 'Work',
color: { light: '#0000FF' }
} as unknown as Calendar
const mockTempCalendar: Calendar = {
id: "temp1",
name: "Temporary",
color: { light: "#00FF00" },
} as unknown as Calendar;
id: 'temp1',
name: 'Temporary',
color: { light: '#00FF00' }
} as unknown as Calendar
const mockState = {
calendars: {
list: {
cal1: mockCalendar1,
cal2: mockCalendar2,
cal2: mockCalendar2
},
templist: {
temp1: mockTempCalendar,
},
},
} as unknown as Partial<RootState>;
temp1: mockTempCalendar
}
}
} as unknown as Partial<RootState>
it("should find calendar in main list", () => {
const result = findCalendarById(mockState, "cal1");
it('should find calendar in main list', () => {
const result = findCalendarById(mockState, 'cal1')
expect(result?.calendar).toEqual(mockCalendar1);
expect(result?.type).toBeUndefined();
});
expect(result?.calendar).toEqual(mockCalendar1)
expect(result?.type).toBeUndefined()
})
it("should find calendar in temp list", () => {
const result = findCalendarById(mockState, "temp1");
it('should find calendar in temp list', () => {
const result = findCalendarById(mockState, 'temp1')
expect(result?.calendar).toEqual(mockTempCalendar);
expect(result?.type).toBe("temp");
});
expect(result?.calendar).toEqual(mockTempCalendar)
expect(result?.type).toBe('temp')
})
it("should not return calendar for non-existent id", () => {
const result = findCalendarById(mockState, "nonexistent");
it('should not return calendar for non-existent id', () => {
const result = findCalendarById(mockState, 'nonexistent')
expect(result).toBeUndefined();
});
expect(result).toBeUndefined()
})
it("should not return calendar for empty string", () => {
const result = findCalendarById(mockState, "");
it('should not return calendar for empty string', () => {
const result = findCalendarById(mockState, '')
expect(result).toBeUndefined();
});
expect(result).toBeUndefined()
})
it("should prioritize temp list over main list", () => {
it('should prioritize temp list over main list', () => {
const stateWithDuplicate = {
calendars: {
list: {
dup1: mockCalendar1,
dup1: mockCalendar1
},
templist: {
dup1: mockTempCalendar,
},
},
} as unknown as Partial<RootState>;
dup1: mockTempCalendar
}
}
} as unknown as Partial<RootState>
const result = findCalendarById(stateWithDuplicate, "dup1");
const result = findCalendarById(stateWithDuplicate, 'dup1')
expect(result?.calendar).toEqual(mockTempCalendar);
expect(result?.type).toBe("temp");
});
expect(result?.calendar).toEqual(mockTempCalendar)
expect(result?.type).toBe('temp')
})
it("should handle undefined list or templist", () => {
it('should handle undefined list or templist', () => {
const stateWithPartialCalendars = {
calendars: {
list: undefined,
templist: { temp1: mockTempCalendar },
},
} as unknown as Partial<RootState>;
templist: { temp1: mockTempCalendar }
}
} as unknown as Partial<RootState>
const result = findCalendarById(stateWithPartialCalendars, "temp1");
expect(result?.calendar).toEqual(mockTempCalendar);
});
const result = findCalendarById(stateWithPartialCalendars, 'temp1')
expect(result?.calendar).toEqual(mockTempCalendar)
})
it("should handle missing calendars state", () => {
const emptyState = {};
it('should handle missing calendars state', () => {
const emptyState = {}
const result = findCalendarById(emptyState, "cal1");
const result = findCalendarById(emptyState, 'cal1')
expect(result).toBeUndefined();
});
});
expect(result).toBeUndefined()
})
})
+72 -72
View File
@@ -1,92 +1,92 @@
import { userData } from "@/features/User/userDataTypes";
import { getUserDisplayName } from "@/utils/userUtils";
import { userData } from '@/features/User/userDataTypes'
import { getUserDisplayName } from '@/utils/userUtils'
describe("userUtils", () => {
describe("getUserDisplayName", () => {
it("returns full name when user has name and family_name", () => {
describe('userUtils', () => {
describe('getUserDisplayName', () => {
it('returns full name when user has name and family_name', () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "Doe",
given_name: "John",
name: "John",
sid: "mockSid",
};
sub: 'test',
email: 'test@test.com',
family_name: 'Doe',
given_name: 'John',
name: 'John',
sid: 'mockSid'
}
expect(getUserDisplayName(user)).toBe("John Doe");
});
expect(getUserDisplayName(user)).toBe('John Doe')
})
it("returns email when user does not have name and family_name", () => {
it('returns email when user does not have name and family_name', () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
sub: 'test',
email: 'test@test.com',
family_name: '',
given_name: '',
name: '',
sid: 'mockSid'
}
expect(getUserDisplayName(user)).toBe("test@test.com");
});
expect(getUserDisplayName(user)).toBe('test@test.com')
})
it("returns email when user has only email", () => {
it('returns email when user has only email', () => {
const user: userData = {
sub: "test",
email: "user@example.com",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
sub: 'test',
email: 'user@example.com',
family_name: '',
given_name: '',
name: '',
sid: 'mockSid'
}
expect(getUserDisplayName(user)).toBe("user@example.com");
});
expect(getUserDisplayName(user)).toBe('user@example.com')
})
it("returns empty string when user is null", () => {
expect(getUserDisplayName(null)).toBe("");
});
it('returns empty string when user is null', () => {
expect(getUserDisplayName(null)).toBe('')
})
it("returns empty string when user is undefined", () => {
expect(getUserDisplayName(undefined)).toBe("");
});
it('returns empty string when user is undefined', () => {
expect(getUserDisplayName(undefined)).toBe('')
})
it("returns empty string when user has no name, family_name, or email", () => {
it('returns empty string when user has no name, family_name, or email', () => {
const user: userData = {
sub: "test",
email: "",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
sub: 'test',
email: '',
family_name: '',
given_name: '',
name: '',
sid: 'mockSid'
}
expect(getUserDisplayName(user)).toBe("");
});
expect(getUserDisplayName(user)).toBe('')
})
it("handles user with only name (no family_name)", () => {
it('handles user with only name (no family_name)', () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "",
given_name: "John",
name: "John",
sid: "mockSid",
};
sub: 'test',
email: 'test@test.com',
family_name: '',
given_name: 'John',
name: 'John',
sid: 'mockSid'
}
expect(getUserDisplayName(user)).toBe("test@test.com");
});
expect(getUserDisplayName(user)).toBe('test@test.com')
})
it("handles user with only family_name (no name)", () => {
it('handles user with only family_name (no name)', () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "Doe",
given_name: "",
name: "",
sid: "mockSid",
};
sub: 'test',
email: 'test@test.com',
family_name: 'Doe',
given_name: '',
name: '',
sid: 'mockSid'
}
expect(getUserDisplayName(user)).toBe("test@test.com");
});
});
});
expect(getUserDisplayName(user)).toBe('test@test.com')
})
})
})