Repository navigation
RD-T49 Store and api fixes #156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| interface MockEndpoint { | ||
| get: jest.Mock; | ||
| post: jest.Mock; | ||
| } | ||
|
|
||
| // The module under test creates its endpoints at import time, so the registry lives inside the mock factory. | ||
| jest.mock('../../common/client', () => { | ||
| const endpoints: Record<string, { get: jest.Mock; post: jest.Mock }> = {}; | ||
| return { | ||
| __endpoints: endpoints, | ||
| createApiEndpoint: (path: string) => { | ||
| const api = { get: jest.fn(), post: jest.fn() }; | ||
| endpoints[path] = api; | ||
| return api; | ||
| }, | ||
| }; | ||
| }); | ||
|
|
||
| import { getCalendarItem, getCalendarItems, getCalendarItemsForDateRange, getCalendarItemTypes, setCalendarAttending } from '../calendar'; | ||
|
|
||
| const mockEndpoints: Record<string, MockEndpoint> = jest.requireMock('../../common/client').__endpoints; | ||
|
|
||
| const endpoint = (path: string) => { | ||
| const api = mockEndpoints[path]; | ||
| if (!api) throw new Error(`endpoint ${path} was not created`); | ||
| return api; | ||
| }; | ||
|
|
||
| describe('calendar api', () => { | ||
| beforeEach(() => { | ||
| Object.values(mockEndpoints).forEach((api) => { | ||
| api.get.mockReset().mockResolvedValue({ data: { Data: [] } }); | ||
| api.post.mockReset().mockResolvedValue({ data: { Id: '' } }); | ||
| }); | ||
| }); | ||
|
|
||
| it('only creates routes that exist on the v4 Calendar controller', () => { | ||
| expect(Object.keys(mockEndpoints).sort()).toEqual([ | ||
| '/Calendar/GetCalendarItem', | ||
| '/Calendar/GetDepartmentCalendarItemTypes', | ||
| '/Calendar/GetDepartmentCalendarItems', | ||
| '/Calendar/GetDepartmentCalendarItemsInRange', | ||
| '/Calendar/SetCalendarAttendingStatus', | ||
| ]); | ||
| }); | ||
|
|
||
| it('reads the department calendar items', async () => { | ||
| await getCalendarItems(); | ||
|
|
||
| expect(endpoint('/Calendar/GetDepartmentCalendarItems').get).toHaveBeenCalledWith(); | ||
| }); | ||
|
|
||
| it('sends the calendar item id as id', async () => { | ||
| await getCalendarItem('42'); | ||
|
|
||
| expect(endpoint('/Calendar/GetCalendarItem').get).toHaveBeenCalledWith({ id: '42' }); | ||
| }); | ||
|
|
||
| it('reads the department calendar item types', async () => { | ||
| await getCalendarItemTypes(); | ||
|
|
||
| expect(endpoint('/Calendar/GetDepartmentCalendarItemTypes').get).toHaveBeenCalledWith(); | ||
| }); | ||
|
|
||
| it('sends the range as start and end without pre-encoding', async () => { | ||
| await getCalendarItemsForDateRange('2026-10-01T00:00:00.000Z', '2026-10-31T23:59:59.000Z'); | ||
|
|
||
| expect(endpoint('/Calendar/GetDepartmentCalendarItemsInRange').get).toHaveBeenCalledWith({ | ||
| start: '2026-10-01T00:00:00.000Z', | ||
| end: '2026-10-31T23:59:59.000Z', | ||
| }); | ||
| }); | ||
|
|
||
| it('posts attending as CalendarItemAttendInput Type 1', async () => { | ||
| await setCalendarAttending({ calendarItemId: '42', attending: true, note: 'On my way' }); | ||
|
|
||
| expect(endpoint('/Calendar/SetCalendarAttendingStatus').post).toHaveBeenCalledWith({ CalendarEventId: '42', Type: 1, Note: 'On my way' }); | ||
| }); | ||
|
|
||
| it('posts not attending as Type 4 with an empty note', async () => { | ||
| await setCalendarAttending({ calendarItemId: '42', attending: false }); | ||
|
|
||
| expect(endpoint('/Calendar/SetCalendarAttendingStatus').post).toHaveBeenCalledWith({ CalendarEventId: '42', Type: 4, Note: '' }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,17 +4,17 @@ | |
| */ | ||
|
|
||
| import { type CalendarItemResult } from '@/models/v4/calendar/calendarItemResult'; | ||
| import { type CalendarItemResultData } from '@/models/v4/calendar/calendarItemResultData'; | ||
| import { type CalendarItemsResult } from '@/models/v4/calendar/calendarItemsResult'; | ||
| import { type CalendarItemTypesResult } from '@/models/v4/calendar/calendarItemTypesResult'; | ||
| import { type SetCalendarAttendingResult } from '@/models/v4/calendar/setCalendarAttendingResult'; | ||
|
|
||
| import { createApiEndpoint } from '../common/client'; | ||
|
|
||
| const calendarApi = createApiEndpoint('/Calendar/GetCalendarItems'); | ||
| const calendarApi = createApiEndpoint('/Calendar/GetDepartmentCalendarItems'); | ||
| const calendarItemApi = createApiEndpoint('/Calendar/GetCalendarItem'); | ||
| const calendarItemTypesApi = createApiEndpoint('/Calendar/GetCalendarItemTypes'); | ||
| const calendarItemsForDateRangeApi = createApiEndpoint('/Calendar/GetCalendarItemsForDateRange'); | ||
| const setCalendarAttendingApi = createApiEndpoint('/Calendar/SetCalendarAttending'); | ||
| const calendarItemTypesApi = createApiEndpoint('/Calendar/GetDepartmentCalendarItemTypes'); | ||
| const calendarItemsForDateRangeApi = createApiEndpoint('/Calendar/GetDepartmentCalendarItemsInRange'); | ||
| const setCalendarAttendingApi = createApiEndpoint('/Calendar/SetCalendarAttendingStatus'); | ||
|
|
||
| /** | ||
| * Get all calendar items | ||
|
|
@@ -28,7 +28,7 @@ export const getCalendarItems = async (): Promise<CalendarItemsResult> => { | |
| * Get a specific calendar item by ID | ||
| */ | ||
| export const getCalendarItem = async (itemId: string): Promise<CalendarItemResult> => { | ||
| const response = await calendarItemApi.get<CalendarItemResult>({ itemId }); | ||
| const response = await calendarItemApi.get<CalendarItemResult>({ id: itemId }); | ||
| return response.data; | ||
| }; | ||
|
|
||
|
|
@@ -45,15 +45,19 @@ export const getCalendarItemTypes = async (): Promise<CalendarItemTypesResult> = | |
| */ | ||
| export const getCalendarItemsForDateRange = async (startDate: string, endDate: string): Promise<CalendarItemsResult> => { | ||
| const response = await calendarItemsForDateRangeApi.get<CalendarItemsResult>({ | ||
| startDate, | ||
| endDate, | ||
| start: startDate, | ||
| end: endDate, | ||
| }); | ||
| return response.data; | ||
| }; | ||
|
|
||
| /** | ||
| * Set calendar attendance | ||
| * Set calendar attendance (Type 1 = attending, 4 = not attending) | ||
| */ | ||
| export const setCalendarAttending = async ({ calendarItemId, attending, note }: { calendarItemId: string; attending: boolean; note?: string }): Promise<void> => { | ||
| await setCalendarAttendingApi.post({ itemId: calendarItemId, attending, note }); | ||
| await setCalendarAttendingApi.post<SetCalendarAttendingResult>({ | ||
| CalendarEventId: calendarItemId, | ||
| Type: attending === true ? 1 : 4, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| Note: note || '', | ||
| }); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| jest.mock('../../common/client', () => ({ | ||
| createApiEndpoint: jest.fn(() => ({ | ||
| get: jest.fn(), | ||
| post: jest.fn(), | ||
| put: jest.fn(), | ||
| delete: jest.fn(), | ||
| })), | ||
| })); | ||
|
|
||
| import { createApiEndpoint } from '../../common/client'; | ||
| import { toggleCallTimers } from '../checkInTimers'; | ||
|
|
||
| const mockCreateApiEndpoint = createApiEndpoint as jest.MockedFunction<typeof createApiEndpoint>; | ||
|
|
||
| describe('toggleCallTimers', () => { | ||
| it('sends callId and enabled in the query string with an empty body', async () => { | ||
| const endpoint = { get: jest.fn(), post: jest.fn(), put: jest.fn().mockResolvedValue({ data: { Id: '' } }), delete: jest.fn() }; | ||
| mockCreateApiEndpoint.mockReturnValueOnce(endpoint as unknown as ReturnType<typeof createApiEndpoint>); | ||
|
|
||
| await toggleCallTimers(101, false); | ||
|
|
||
| expect(mockCreateApiEndpoint).toHaveBeenLastCalledWith('/CheckInTimers/ToggleCallTimers?callId=101&enabled=false'); | ||
| expect(endpoint.put).toHaveBeenCalledWith({}); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| interface MockEndpoint { | ||
| get: jest.Mock; | ||
| post: jest.Mock; | ||
| } | ||
|
|
||
| // The module under test creates its endpoints at import time, so the registry lives inside the mock factory. | ||
| jest.mock('../../common/client', () => { | ||
| const endpoints: Record<string, { get: jest.Mock; post: jest.Mock }> = {}; | ||
| return { | ||
| __endpoints: endpoints, | ||
| createApiEndpoint: (path: string) => { | ||
| const api = { get: jest.fn(), post: jest.fn() }; | ||
| endpoints[path] = api; | ||
| return api; | ||
| }, | ||
| }; | ||
| }); | ||
|
|
||
| import { getAllShifts, getShift, getShiftDay, getTodaysShifts, signupForShiftDay } from '../shifts'; | ||
|
|
||
| const mockEndpoints: Record<string, MockEndpoint> = jest.requireMock('../../common/client').__endpoints; | ||
|
|
||
| const endpoint = (path: string) => { | ||
| const api = mockEndpoints[path]; | ||
| if (!api) throw new Error(`endpoint ${path} was not created`); | ||
| return api; | ||
| }; | ||
|
|
||
| describe('shifts api', () => { | ||
| beforeEach(() => { | ||
| Object.values(mockEndpoints).forEach((api) => { | ||
| api.get.mockReset().mockResolvedValue({ data: { Data: [] } }); | ||
| api.post.mockReset().mockResolvedValue({ data: { Id: '' } }); | ||
| }); | ||
| }); | ||
|
|
||
| it('only creates routes that exist on the v4 Shifts controller', () => { | ||
| expect(Object.keys(mockEndpoints).sort()).toEqual(['/Shifts/GetShift', '/Shifts/GetShiftDay', '/Shifts/GetShifts', '/Shifts/GetTodaysShifts', '/Shifts/SignupForShiftDay']); | ||
| }); | ||
|
|
||
| it('reads all shifts from GetShifts', async () => { | ||
| await getAllShifts(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled promise rejection handling leaves the awaited API operation's outcome unverified, allowing failures to escape test assertions. Add explicit promise rejection handling with an assertion or try/catch. Kody rule violation: Handle async operations with proper error handling await expect(getAllShifts()).resolves.toBeDefined();Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
|
|
||
| expect(endpoint('/Shifts/GetShifts').get).toHaveBeenCalledWith(); | ||
| }); | ||
|
|
||
| it('sends the shift id as id', async () => { | ||
| await getShift('7'); | ||
|
|
||
| expect(endpoint('/Shifts/GetShift').get).toHaveBeenCalledWith({ id: '7' }); | ||
| }); | ||
|
|
||
| it('sends the shift day id as id', async () => { | ||
| await getShiftDay('12'); | ||
|
|
||
| expect(endpoint('/Shifts/GetShiftDay').get).toHaveBeenCalledWith({ id: '12' }); | ||
| }); | ||
|
|
||
| it("reads today's shift days", async () => { | ||
| await getTodaysShifts(); | ||
|
|
||
| expect(endpoint('/Shifts/GetTodaysShifts').get).toHaveBeenCalledWith(); | ||
| }); | ||
|
|
||
| it('posts ShiftDaySignupInput with numeric ids', async () => { | ||
| endpoint('/Shifts/SignupForShiftDay').post.mockResolvedValue({ data: { Id: '99' } }); | ||
|
|
||
| await expect(signupForShiftDay('12', '3')).resolves.toEqual({ Id: '99' }); | ||
| expect(endpoint('/Shifts/SignupForShiftDay').post).toHaveBeenCalledWith({ ShiftDayId: 12, GroupId: 3 }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: Resgrid/Dispatch
Length of output: 45634
Script executed:
Repository: Resgrid/Dispatch
Length of output: 7116
Web query:
Resgrid Core CalendarController SetCalendarAttendingStatus Status failure response contract deployed APIResult:
Script executed:
Repository: Resgrid/Dispatch
Length of output: 32243
Script executed:
Repository: Resgrid/Dispatch
Length of output: 34332
Script executed:
Repository: Resgrid/Dispatch
Length of output: 9435
Handle unsuccessful attendance results.
SetCalendarAttendingStatusreturns HTTP 200 withStatus: "failure"whenSignupForEventreturnsnull.setCalendarAttendingdiscards that response, so the store updates its local attendance arrays and leavesattendanceErrorclear.Inspect the response status and throw on failure.
Suggested fix