Skip to content

Commit

Permalink
test: adds setup for mock-service-worker (#127)
Browse files Browse the repository at this point in the history
* test: add msw and basic handlers for a few types

* test: add mock data for a basic workflow execution

* test: fixing/removing tests after adding msw

* test: throw on unexpected requests to msw

* fix: upgrade TS to fix error and cleanup resulting errors
  • Loading branch information
schottra committed Jan 4, 2021
1 parent be9fd46 commit e16bb82
Show file tree
Hide file tree
Showing 34 changed files with 1,024 additions and 838 deletions.
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
"@types/dom-helpers": "^3.4.1",
"@types/express": "^4.17.2",
"@types/html-webpack-plugin": "^2.28.0",
"@types/jest": "^25.2.1",
"@types/jest": "^26.0.0",
"@types/js-yaml": "^3.10.1",
"@types/linkify-it": "^2.1.0",
"@types/lodash": "^4.14.68",
Expand Down Expand Up @@ -138,7 +138,7 @@
"axios": "^0.18.1",
"axios-mock-adapter": "^1.16.0",
"babel-core": "^7.0.0-0",
"babel-jest": "^25.5.0",
"babel-jest": "^26.0.0",
"babel-loader": "^8.0.0-beta.2",
"camelcase-keys": "^6.1.1",
"classnames": "^2.2.6",
Expand All @@ -160,13 +160,14 @@
"husky": "^4.2.5",
"identity-obj-proxy": "^3.0.0",
"intersection-observer": "^0.7.0",
"jest": "^25.5.0",
"jest": "^26.0.0",
"linkify-it": "^2.2.0",
"lint-staged": "^7.0.4",
"lossless-json": "^1.0.3",
"memoize-one": "^5.0.0",
"moment": "^2.18.1",
"moment-timezone": "^0.5.28",
"msw": "^0.24.1",
"object-hash": "^1.3.1",
"prettier": "1.19.1",
"protobufjs": "~6.8.0",
Expand All @@ -189,15 +190,15 @@
"snakecase-keys": "^3.1.0",
"source-map-loader": "^0.2.1",
"storybook-react-router": "^1.0.5",
"ts-jest": "^25.4.0",
"ts-jest": "^26.3.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.0.2",
"tslint": "^5.20.1",
"tslint-config-airbnb": "^5.11.2",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"tslint-react": "^4.1.0",
"typescript": "^3.8.3",
"typescript": "^4.0.0",
"uglifyjs-webpack-plugin": "^1.2.5",
"url-search-params": "^0.10.0",
"use-react-router": "^1.0.7",
Expand Down
11 changes: 1 addition & 10 deletions src/common/env.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { Env } from 'config';

/** Safely check for server environment */
export function isServer() {
try {
return __isServer;
} catch (e) {
return false;
}
}

/** equivalent to process.env in server and client */
// tslint:disable-next-line:no-any
export const env: Env = (isServer() ? process.env : window.env) as any;
export const env: Env = Object.assign({}, process.env, window.env);
4 changes: 3 additions & 1 deletion src/common/test/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
timestampToDate
} from '../utils';

jest.mock('common/env');
jest.mock('common/env', () => ({
env: jest.requireActual('common/env').env
}));

import { Protobuf } from 'flyteidl';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import * as React from 'react';

import { SectionHeader } from 'components/common';
import { useCommonStyles } from 'components/common/styles';
import { NodeDetailsProps } from 'components/WorkflowGraph';
import { useStyles as useBaseStyles } from 'components/WorkflowGraph/NodeDetails/styles';

import { LiteralMapViewer } from 'components/Literals';
import * as React from 'react';
import { ExecutionContext } from '../../contexts';

/** Details panel renderer for the start/input node in a graph. Displays the
Expand All @@ -23,15 +20,7 @@ export const InputNodeDetails: React.FC<NodeDetailsProps> = () => {
<SectionHeader title="Execution Inputs" />
</div>
</header>
<div className={baseStyles.content}>
<div className={commonStyles.detailsPanelCard}>
<div className={commonStyles.detailsPanelCardContent}>
<LiteralMapViewer
map={execution.closure.computedInputs}
/>
</div>
</div>
</div>
<div className={baseStyles.content} />
</section>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ describe.skip('ExecutionNodeViews', () => {
props = { execution: workflowExecution };
});

it('is disabled', () => {});

const renderViews = () =>
render(
<QueryClientProvider client={queryClient}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ describe.skip('NodeExecutionsTable', () => {
return render(<Table {...await getProps()} />);
};

it('is disabled', () => {});

// it('renders task name for task nodes', async () => {
// const { queryAllByText, getAllByRole } = await renderTable();
// await waitFor(() => getAllByRole('listitem').length > 0);
Expand Down
15 changes: 0 additions & 15 deletions src/components/Executions/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@ describe('getWorkflowExecutionTimingMS', () => {
expect(getWorkflowExecutionTimingMS(execution)).toBeNull();
});

it('should return null when no createdAt field is present', () => {
delete execution.closure.createdAt;
expect(getWorkflowExecutionTimingMS(execution)).toBeNull();
});

it('should return null when no startedAt field is present', () => {
delete execution.closure.startedAt;
expect(getWorkflowExecutionTimingMS(execution)).toBeNull();
Expand Down Expand Up @@ -103,11 +98,6 @@ describe('getNodeExecutionTimingMS', () => {
expect(getNodeExecutionTimingMS(execution)).toBeNull();
});

it('should return null when no createdAt field is present', () => {
delete execution.closure.createdAt;
expect(getNodeExecutionTimingMS(execution)).toBeNull();
});

it('should return null when no startedAt field is present', () => {
delete execution.closure.startedAt;
expect(getNodeExecutionTimingMS(execution)).toBeNull();
Expand Down Expand Up @@ -151,11 +141,6 @@ describe('getTaskExecutionTimingMS', () => {
expect(getTaskExecutionTimingMS(execution)).toBeNull();
});

it('should return null when no createdAt field is present', () => {
delete execution.closure.createdAt;
expect(getTaskExecutionTimingMS(execution)).toBeNull();
});

it('should return null when no startedAt field is present', () => {
delete execution.closure.startedAt;
expect(getTaskExecutionTimingMS(execution)).toBeNull();
Expand Down
6 changes: 6 additions & 0 deletions src/mocks/data/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Arbitrary start date used as a basis for generating timestamps
// to keep the mocked data consistent across runs
export const mockStartDate = new Date('2020-11-15T02:32:19.610Z');

// Workflow Execution duration in milliseconds
export const defaultWorkflowExecutionDuration = 1000 * 60 * 60 * 1.251;
15 changes: 15 additions & 0 deletions src/mocks/data/launchPlans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Core } from 'flyteidl';
import { LaunchPlan } from 'models';

// TODO:
const basic: LaunchPlan = {
id: {
resourceType: Core.ResourceType.LAUNCH_PLAN,
project: 'flytetest',
domain: 'development',
name: 'Basic',
version: 'abc123'
}
} as LaunchPlan;

export const launchPlans = { basic };
Empty file.
8 changes: 8 additions & 0 deletions src/mocks/data/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function emptyProject(id: string, name?: string) {
return {
id,
name: name ?? id,
domains: [],
description: ''
};
}
48 changes: 48 additions & 0 deletions src/mocks/data/workflowExecutions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { dateToTimestamp, millisecondsToDuration } from 'common/utils';
import { Admin } from 'flyteidl';
import { LiteralMap } from 'models/Common/types';
import { WorkflowExecutionPhase } from 'models/Execution/enums';
import { Execution, ExecutionMetadata } from 'models/Execution/types';
import { defaultWorkflowExecutionDuration, mockStartDate } from './constants';
import { launchPlans } from './launchPlans';
import { workflows } from './workflows';

export function defaultWorkflowExecutionMetadata(): ExecutionMetadata {
return {
mode: Admin.ExecutionMetadata.ExecutionMode.MANUAL,
principal: 'sdk',
nesting: 0
};
}

export function emptyLiteralMap(): LiteralMap {
return { literals: {} };
}

const basic: Execution = {
id: {
project: 'flytetest',
domain: 'development',
name: 'abc123'
},
spec: {
launchPlan: { ...launchPlans.basic.id },
inputs: emptyLiteralMap(),
metadata: defaultWorkflowExecutionMetadata(),
notifications: {
notifications: []
}
},
closure: {
computedInputs: emptyLiteralMap(),
createdAt: dateToTimestamp(mockStartDate),
duration: millisecondsToDuration(defaultWorkflowExecutionDuration),
phase: WorkflowExecutionPhase.SUCCEEDED,
startedAt: dateToTimestamp(mockStartDate),
workflowId: { ...workflows.basic.id }
}
};

export const workflowExecutions = {
basic
};
15 changes: 15 additions & 0 deletions src/mocks/data/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Core } from 'flyteidl';
import { Workflow } from 'models/Workflow/types';

// TODO:
const basic: Workflow = {
id: {
resourceType: Core.ResourceType.WORKFLOW,
project: 'flytetest',
domain: 'development',
name: 'Basic',
version: 'abc123'
}
};

export const workflows = { basic };
10 changes: 10 additions & 0 deletions src/mocks/getDefaultData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RequestHandlersList } from 'msw/lib/types/setupWorker/glossary';
import { workflowExecutions } from './data/workflowExecutions';
import { workflowExecutionHandler } from './handlers';

export function getDefaultData(): RequestHandlersList {
const workflowExecutionHandlers = Object.values(workflowExecutions).map(
workflowExecutionHandler
);
return [...workflowExecutionHandlers];
}
102 changes: 102 additions & 0 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Admin } from 'flyteidl';
import {
EncodableType,
encodeProtoPayload,
Execution,
NameIdentifierScope,
NodeExecution,
Project,
Workflow
} from 'models';
import {
makeExecutionPath,
makeNodeExecutionListPath,
makeNodeExecutionPath
} from 'models/Execution/utils';
import { makeWorkflowPath } from 'models/Workflow/utils';
import { ResponseResolver, rest } from 'msw';
import { setupServer } from 'msw/lib/types/node';
import { RestContext } from 'msw/lib/types/rest';
import { apiPath } from './utils';

export function adminEntityResponder(
data: any,
encodeType: EncodableType<any>
): ResponseResolver<any, RestContext> {
const buffer = encodeProtoPayload(data, encodeType);
const contentLength = buffer.byteLength.toString();
return (_, res, ctx) =>
res(
ctx.set('Content-Type', 'application/octet-stream'),
ctx.set('Content-Length', contentLength),
ctx.body(buffer)
);
}

export function workflowExecutionHandler(data: Partial<Execution>) {
return rest.get(
apiPath(makeExecutionPath(data.id!)),
adminEntityResponder(data, Admin.Execution)
);
}

export function workflowHandler(data: Partial<Workflow>) {
return rest.get(
apiPath(makeWorkflowPath(data.id!)),
adminEntityResponder(data, Admin.Workflow)
);
}

export function nodeExecutionHandler(data: Partial<NodeExecution>) {
return rest.get(
apiPath(makeNodeExecutionPath(data.id!)),
adminEntityResponder(data, Admin.NodeExecution)
);
}

// TODO: pagination responder that respects limit/token?
export function nodeExecutionListHandler(
scope: NameIdentifierScope,
data: Partial<NodeExecution>[]
) {
return rest.get(
apiPath(makeNodeExecutionListPath(scope)),
adminEntityResponder(
{
nodeExecutions: data
},
Admin.NodeExecutionList
)
);
}

export function projectListHandler(data: Project[]) {
return rest.get(
apiPath('/projects'),
adminEntityResponder({ projects: data }, Admin.Projects)
);
}

export interface BoundAdminServer {
insertNodeExecution(data: Partial<NodeExecution>): void;
insertNodeExecutionList(
scope: NameIdentifierScope,
data: Partial<NodeExecution>[]
): void;
insertProjects(data: Project[]): void;
insertWorkflow(data: Partial<Workflow>): void;
insertWorkflowExecution(data: Partial<Execution>): void;
}

export function bindHandlers({
use
}: ReturnType<typeof setupServer>): BoundAdminServer {
return {
insertNodeExecution: data => use(nodeExecutionHandler(data)),
insertNodeExecutionList: (scope, data) =>
use(nodeExecutionListHandler(scope, data)),
insertProjects: data => use(projectListHandler(data)),
insertWorkflow: data => use(workflowHandler(data)),
insertWorkflowExecution: data => use(workflowExecutionHandler(data))
};
}
7 changes: 7 additions & 0 deletions src/mocks/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { setupServer } from 'msw/node';
import { getDefaultData } from './getDefaultData';
import { bindHandlers } from './handlers';

const server = setupServer(...getDefaultData());
const handlers = bindHandlers(server);
export const mockServer = { ...server, ...handlers };
5 changes: 5 additions & 0 deletions src/mocks/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { apiPrefix } from 'models/AdminEntity/constants';

export function apiPath(path: string) {
return `${apiPrefix}${path}`;
}
2 changes: 2 additions & 0 deletions src/models/AdminEntity/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RequestConfig } from './types';

export const apiPrefix = '/api/v1';

export const limits = {
DEFAULT: 25,
/** The admin API requires a limit value for all list endpoints, but does not
Expand Down
Loading

0 comments on commit e16bb82

Please sign in to comment.