diff --git a/packages/manager/.changeset/pr-10937-tech-stories-1728079160083.md b/packages/manager/.changeset/pr-10937-tech-stories-1728079160083.md new file mode 100644 index 00000000000..de1e74c498e --- /dev/null +++ b/packages/manager/.changeset/pr-10937-tech-stories-1728079160083.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Add support ticket mocks to MSW 2.0 ([#10937](https://github.com/linode/manager/pull/10937)) diff --git a/packages/manager/src/dev-tools/load.ts b/packages/manager/src/dev-tools/load.ts index 6d34f875c2d..92332537fcf 100644 --- a/packages/manager/src/dev-tools/load.ts +++ b/packages/manager/src/dev-tools/load.ts @@ -113,6 +113,14 @@ export async function loadDevTools( ...(seedContext?.regionAvailability || []), ], regions: [...initialContext.regions, ...(seedContext?.regions || [])], + supportReplies: [ + ...initialContext.supportReplies, + ...(seedContext?.supportReplies || []), + ], + supportTickets: [ + ...initialContext.supportTickets, + ...(seedContext?.supportTickets || []), + ], volumes: [...initialContext.volumes, ...(seedContext?.volumes || [])], }; diff --git a/packages/manager/src/mocks/mockState.ts b/packages/manager/src/mocks/mockState.ts index 429794b7365..3cbf770964f 100644 --- a/packages/manager/src/mocks/mockState.ts +++ b/packages/manager/src/mocks/mockState.ts @@ -30,6 +30,8 @@ export const emptyStore: MockState = { placementGroups: [], regionAvailability: [], regions: [], + supportReplies: [], + supportTickets: [], volumes: [], }; diff --git a/packages/manager/src/mocks/presets/baseline/crud.ts b/packages/manager/src/mocks/presets/baseline/crud.ts index 3fc0fd6db79..1719f7772fd 100644 --- a/packages/manager/src/mocks/presets/baseline/crud.ts +++ b/packages/manager/src/mocks/presets/baseline/crud.ts @@ -5,6 +5,7 @@ import { import { linodeCrudPreset } from 'src/mocks/presets/crud/linodes'; import { placementGroupsCrudPreset } from '../crud/placementGroups'; +import { supportTicketCrudPreset } from '../crud/supportTickets'; import { volumeCrudPreset } from '../crud/volumes'; import type { MockPresetBaseline } from 'src/mocks/types'; @@ -14,6 +15,7 @@ export const baselineCrudPreset: MockPresetBaseline = { handlers: [ ...linodeCrudPreset.handlers, ...placementGroupsCrudPreset.handlers, + ...supportTicketCrudPreset.handlers, ...volumeCrudPreset.handlers, // Events. diff --git a/packages/manager/src/mocks/presets/crud/handlers/supportTickets.ts b/packages/manager/src/mocks/presets/crud/handlers/supportTickets.ts new file mode 100644 index 00000000000..89a24e35644 --- /dev/null +++ b/packages/manager/src/mocks/presets/crud/handlers/supportTickets.ts @@ -0,0 +1,184 @@ +import { DateTime } from 'luxon'; +import { http } from 'msw'; + +import { supportReplyFactory, supportTicketFactory } from 'src/factories'; +import { mswDB } from 'src/mocks/indexedDB'; +import { queueEvents } from 'src/mocks/utilities/events'; +import { + makeNotFoundResponse, + makePaginatedResponse, + makeResponse, +} from 'src/mocks/utilities/response'; + +import type { SupportReply, SupportTicket } from '@linode/api-v4'; +import type { StrictResponse } from 'msw'; +import type { MockState } from 'src/mocks/types'; +import type { + APIErrorResponse, + APIPaginatedResponse, +} from 'src/mocks/utilities/response'; + +export const createSupportTicket = (mockState: MockState) => [ + http.post( + '*/support/tickets', + async ({ + request, + }): Promise> => { + const payload = await request.clone().json(); + + const supportTicket = supportTicketFactory.build({ + closable: true, + description: payload['description'], + entity: { + id: 1, + label: 'mock-linode-1', + type: 'linode', + url: '/v4/linode/instances/1', + }, + opened: DateTime.now().toISO(), + severity: 1, + status: 'open', + summary: payload['summary'], + updated: DateTime.now().toISO(), + }); + + await mswDB.add('supportTickets', supportTicket, mockState); + + queueEvents({ + event: { + action: 'ticket_create', + entity: { + id: supportTicket.id, + label: supportTicket.summary, + type: 'support_ticket', + url: `/v4/support/tickets/${supportTicket.id}`, + }, + }, + mockState, + sequence: [{ status: 'notification' }], + }); + + return makeResponse(supportTicket); + } + ), +]; + +export const getSupportTickets = () => [ + http.get( + '*/support/tickets', + async ({ + request, + }): Promise< + StrictResponse> + > => { + const supportTickets = await mswDB.getAll('supportTickets'); + + if (!supportTickets) { + return makeNotFoundResponse(); + } + return makePaginatedResponse({ + data: supportTickets, + request, + }); + } + ), + http.get( + '*/support/tickets/:ticketId', + async ({ + params, + }): Promise> => { + const id = Number(params.ticketId); + const supportTicket = await mswDB.get('supportTickets', id); + + if (!supportTicket) { + return makeNotFoundResponse(); + } + return makeResponse(supportTicket); + } + ), +]; + +export const closeSupportTicket = (mockState: MockState) => [ + http.post('*/support/tickets/:ticketId/close', async ({ params }) => { + const id = Number(params.ticketId); + const supportTicket = await mswDB.get('supportTickets', id); + + if (!supportTicket) { + return makeNotFoundResponse(); + } + + mswDB.update( + 'supportTickets', + id, + { ...supportTicket, closed: DateTime.now().toISO(), status: 'closed' }, + mockState + ); + + return makeResponse({}); + }), +]; + +export const getSupportTicketReplies = () => [ + http.get( + '*/support/tickets/:ticketId/replies', + async ({ + params, + request, + }): Promise< + StrictResponse> + > => { + const id = Number(params.ticketId); + const supportReplies = await mswDB.get('supportReplies', id); + + if (!supportReplies) { + return makePaginatedResponse({ + data: [], + request, + }); + } + return makePaginatedResponse({ + data: [supportReplies], + request, + }); + } + ), +]; + +export const createSupportTicketReply = (mockState: MockState) => [ + http.post( + '*/support/tickets/:ticketId/replies', + async ({ + params, + request, + }): Promise> => { + const payload = await request.clone().json(); + const id = Number(params.ticketId); + const supportTicket = await mswDB.get('supportTickets', id); + + const supportTicketReply = supportReplyFactory.build({ + created_by: 'test-account', + description: payload['description'], + friendly_name: 'test-account', + from_linode: false, + }); + + await mswDB.add('supportReplies', supportTicketReply, mockState); + + queueEvents({ + event: { + action: 'ticket_update', + entity: { + id: supportTicket?.id ?? -1, + label: supportTicket?.summary ?? null, + type: 'support_ticket', + url: `/v4/support/tickets/${supportTicket?.id}`, + }, + }, + mockState, + sequence: [{ status: 'notification' }], + }); + + return makeResponse(supportTicketReply); + } + ), +]; diff --git a/packages/manager/src/mocks/presets/crud/seeds/index.ts b/packages/manager/src/mocks/presets/crud/seeds/index.ts index ca21c1dff0a..f00084e08bc 100644 --- a/packages/manager/src/mocks/presets/crud/seeds/index.ts +++ b/packages/manager/src/mocks/presets/crud/seeds/index.ts @@ -1,5 +1,11 @@ import { linodesSeeder } from './linodes'; import { placementGroupSeeder } from './placementGroups'; +import { supportTicketsSeeder } from './supportTickets'; import { volumesSeeder } from './volumes'; -export const dbSeeders = [linodesSeeder, placementGroupSeeder, volumesSeeder]; +export const dbSeeders = [ + linodesSeeder, + placementGroupSeeder, + supportTicketsSeeder, + volumesSeeder, +]; diff --git a/packages/manager/src/mocks/presets/crud/seeds/supportTickets.ts b/packages/manager/src/mocks/presets/crud/seeds/supportTickets.ts new file mode 100644 index 00000000000..a29a7538941 --- /dev/null +++ b/packages/manager/src/mocks/presets/crud/seeds/supportTickets.ts @@ -0,0 +1,28 @@ +import { getSeedsCountMap } from 'src/dev-tools/utils'; +import { supportTicketFactory } from 'src/factories'; +import { mswDB } from 'src/mocks/indexedDB'; + +import type { MockSeeder, MockState } from 'src/mocks/types'; + +export const supportTicketsSeeder: MockSeeder = { + canUpdateCount: true, + desc: 'Support Tickets Seeds', + group: { id: 'Support Tickets' }, + id: 'support-tickets:crud', + label: 'Support Tickets', + + seeder: async (mockState: MockState) => { + const seedsCountMap = getSeedsCountMap(); + const count = seedsCountMap[supportTicketsSeeder.id] ?? 0; + const supportTickets = supportTicketFactory.buildList(count); + + const updatedMockState = { + ...mockState, + supportTickets: mockState.supportTickets.concat(supportTickets), + }; + + await mswDB.saveStore(updatedMockState, 'seedState'); + + return updatedMockState; + }, +}; diff --git a/packages/manager/src/mocks/presets/crud/supportTickets.ts b/packages/manager/src/mocks/presets/crud/supportTickets.ts new file mode 100644 index 00000000000..5770eb4a0ef --- /dev/null +++ b/packages/manager/src/mocks/presets/crud/supportTickets.ts @@ -0,0 +1,22 @@ +import { + closeSupportTicket, + createSupportTicket, + createSupportTicketReply, + getSupportTicketReplies, + getSupportTickets, +} from 'src/mocks/presets/crud/handlers/supportTickets'; + +import type { MockPresetCrud } from 'src/mocks/types'; + +export const supportTicketCrudPreset: MockPresetCrud = { + group: { id: 'Support Tickets' }, + handlers: [ + createSupportTicket, + closeSupportTicket, + getSupportTickets, + getSupportTicketReplies, + createSupportTicketReply, + ], + id: 'support-tickets:crud', + label: 'Support Tickets CRUD', +}; diff --git a/packages/manager/src/mocks/types.ts b/packages/manager/src/mocks/types.ts index ed2621243df..8ef5563dc4b 100644 --- a/packages/manager/src/mocks/types.ts +++ b/packages/manager/src/mocks/types.ts @@ -7,6 +7,8 @@ import type { PlacementGroup, Region, RegionAvailability, + SupportReply, + SupportTicket, Volume, } from '@linode/api-v4'; import type { HttpHandler } from 'msw'; @@ -70,11 +72,12 @@ export interface MockPresetExtra extends MockPresetBase { * Mock Preset Crud */ export type MockPresetCrudGroup = { - id: 'Linodes' | 'Placement Groups' | 'Volumes'; + id: 'Linodes' | 'Placement Groups' | 'Support Tickets' | 'Volumes'; }; export type MockPresetCrudId = | 'linodes:crud' | 'placement-groups:crud' + | 'support-tickets:crud' | 'volumes:crud'; export interface MockPresetCrud extends MockPresetBase { canUpdateCount?: boolean; @@ -96,6 +99,8 @@ export interface MockState { placementGroups: PlacementGroup[]; regionAvailability: RegionAvailability[]; regions: Region[]; + supportReplies: SupportReply[]; + supportTickets: SupportTicket[]; volumes: Volume[]; }