Skip to content

Commit

Permalink
WIP event handling, Linode events
Browse files Browse the repository at this point in the history
  • Loading branch information
jdamore-linode committed Jun 18, 2024
1 parent b761122 commit 8070c1b
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 3 deletions.
2 changes: 1 addition & 1 deletion packages/manager/src/factories/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const eventFactory = Factory.Sync.makeFactory<Event>({
type: 'linode',
url: '/v4/linode/instances/30499244',
},
id: Factory.each((id) => id),
id: Factory.each((id) => id + 1),
message: null,
percent_complete: 10,
rate: null,
Expand Down
116 changes: 116 additions & 0 deletions packages/manager/src/mocks/handlers/event-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { StrictResponse, http } from 'msw';
import { getPaginatedSlice } from '../utilities/pagination';
import {
makeNotFoundResponse,
makePaginatedResponse,
makeResponse,
} from '../utilities/response';
import { DateTime } from 'luxon';

import type { APIErrorResponse } from '../utilities/response';
import type { MockContext } from '../mockContext';
import type { Event } from '@linode/api-v4';
import type { MockEventProgressHandler } from '../mockContext';

/**
* Filters events by their `created` date.
*
* Events with `created` dates in the future are filtered out.
*
* @param event - Event to filter.
*
* @returns `true` if event's created date is in the past, `false` otherwise.
*/
const filterEventsByCreatedTime = (
eventQueueItem: [Event, MockEventProgressHandler | null]
): boolean => DateTime.fromISO(eventQueueItem[0].created) <= DateTime.now();

/**
* Simulates event progression by executing a callback that may mutate the event or context.
*
*
*/
const progressEvent = (
eventQueueItem: [Event, MockEventProgressHandler | null],
context: MockContext
) => {
const [event, handler] = eventQueueItem;
if (handler) {
const result = handler(event, context);
// If handler responds with `true`, replace the handler with `null` to prevent further execution.
if (result) {
eventQueueItem[1] = null;
}
}
};

export const getEvents = (mockContext: MockContext) => [
http.get('*/v4*/events', ({ request }) => {
const url = new URL(request.url);

const pageNumber = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 25;
const totalPages = Math.max(
Math.ceil(mockContext.regions.length / pageSize),
1
);

const events = mockContext.eventQueue
.filter(filterEventsByCreatedTime)
.map((queuedEvent) => {
progressEvent(queuedEvent, mockContext);
return queuedEvent[0];
});

const pageSlice = getPaginatedSlice(events, pageNumber, pageSize);

return makePaginatedResponse(pageSlice, pageNumber, totalPages);
}),
http.get(
'*/v4*/events/:id',
({ params }): StrictResponse<Event | APIErrorResponse> => {
const id = Number(params.id);
const event = mockContext.eventQueue.find(
(eventQueueItem) => eventQueueItem[0].id === id
);

if (!event) {
return makeNotFoundResponse();
}

progressEvent(event, mockContext);
return makeResponse(event[0]);
}
),
];

export const updateEvents = (mockContext: MockContext) => [
// Marks all events up to and including the event with the given ID as seen.
http.post('*/v4*/events/:id/seen', ({ params }) => {
const id = Number(params.id);

mockContext.eventQueue.forEach((eventQueueItem) => {
if (eventQueueItem[0].id <= id) {
eventQueueItem[0].seen = true;
}
});

// API-v4 responds with a 200 and empty object regardless of whether the
// requested event actually exists (or belongs to the requesting user).
return makeResponse({});
}),
// Marks all events up to and including the event with the given ID as read.
http.post('*/v4*/events/:id/read', ({ params }) => {
const id = Number(params.id);

mockContext.eventQueue.forEach((eventQueueItem) => {
if (eventQueueItem[0].id <= id) {
eventQueueItem[0].read = true;
}
});

// API-v4 responds with a 200 and empty object regardless of whether the
// requested event actually exists (or belongs to the requesting user).
return makeResponse({});
}),
];
35 changes: 34 additions & 1 deletion packages/manager/src/mocks/handlers/linode-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Config, Linode } from '@linode/api-v4';
import type { MockContext } from 'src/mocks/mockContext';
import type { APIErrorResponse } from 'src/mocks/utilities/response';
import type { StrictResponse } from 'msw';
import { configFactory, linodeFactory } from 'src/factories';
import { configFactory, eventFactory, linodeFactory } from 'src/factories';
import { DateTime } from 'luxon';
import { getPaginatedSlice } from '../utilities/pagination';

Expand Down Expand Up @@ -97,6 +97,7 @@ export const createLinodes = (mockContext: MockContext) => [
region: payload['region'],
created: DateTime.now().toISO(),
image: payload['image'],
status: 'provisioning',
});

// Mock default label behavior when one is not specified.
Expand All @@ -108,8 +109,40 @@ export const createLinodes = (mockContext: MockContext) => [
created: DateTime.now().toISO(),
});

const linodeEvent = eventFactory.build({
action: 'linode_create',
created: DateTime.local().toISO(),
seen: false,
read: false,
duration: null,
rate: null,
percent_complete: 0,
entity: {
label: linode.label,
id: linode.id,
type: 'linode',
url: `/v4/linode/instances/${linode.id}`,
},
status: 'scheduled',
message: '',
});

mockContext.linodes.push(linode);
mockContext.linodeConfigs.push([linode.id, linodeConfig]);
mockContext.eventQueue.push([
linodeEvent,
(e, context) => {
if (e.status === 'scheduled') {
e.status = 'started';
return false;
}
if (e.status === 'started') {
e.status = 'finished';
linode.status = 'booting';
}
return true;
},
]);

return makeResponse(linode);
}),
Expand Down
14 changes: 13 additions & 1 deletion packages/manager/src/mocks/mockContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ import type {
RegionAvailability,
} from '@linode/api-v4';

/**
* Describes a function that executes on each request to the events endpoint.
*
* Can be used to simulate progress or update state in response to an event.
*
* @returns `true` if event is considered complete, `false` if callback should continue to be called.
*/
export type MockEventProgressHandler = (
event: Event,
context: MockContext
) => boolean;

/**
* Contextual data shared among mocks.
*/
Expand All @@ -24,7 +36,7 @@ export interface MockContext {
regionAvailability: RegionAvailability[];

// Misc.
eventQueue: Event[];
eventQueue: [Event, MockEventProgressHandler | null][];
notificationQueue: Notification[];
}

Expand Down
5 changes: 5 additions & 0 deletions packages/manager/src/mocks/presets/baseline/baseline-crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @file Basic CRUD MSW preset.
*/

import { getEvents, updateEvents } from 'src/mocks/handlers/event-handlers';
import type { MockPreset } from '../../mockPreset';
import { getLinodes, createLinodes } from 'src/mocks/handlers/linode-handlers';
import {
Expand All @@ -25,5 +26,9 @@ export const baselineCrudPreset: MockPreset = {
createVolumes,
updateVolumes,
deleteVolumes,

// Events.
getEvents,
updateEvents,
],
};

0 comments on commit 8070c1b

Please sign in to comment.