Skip to content

Commit

Permalink
feat(app,robot-server): add support for sessions API (#5628)
Browse files Browse the repository at this point in the history
* add api v2 structured types

* add session actions and tests

* add reducers plus tests

* add selectors + tests

* selector test fix

* remove meaningless selector

* add epics plus tests

* adding sessions to app

* fix some errors in imports

* fix lint errors.

* robot-server settings endpoint models are camelCase

* better typing, lint and flow error fix progress.

* fix flow errors.

* adding typing todos

* rename session from check to calibrationCheck.

* remove robot from session naming

* remove robot from sessions

* lint errors

* remove sessionId from response. it was not necessary.

* remove sessionId from responses.

* Use MikeC's fancy typing for response v2

* type the v2 links

* use helpers for epic tests

* lose all references to update. use command instead.

* use RsourceLinks type
  • Loading branch information
amitlissack authored May 13, 2020
1 parent 9ba54e4 commit 441d682
Show file tree
Hide file tree
Showing 28 changed files with 1,695 additions and 51 deletions.
2 changes: 1 addition & 1 deletion api/src/opentrons/server/endpoints/calibration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class TiprackPosition(BaseModel):

class SessionType(str, Enum):
"""The available session types"""
check = 'check'
calibration_check = 'calibrationCheck'


class SpecificPipette(BaseModel):
Expand Down
4 changes: 3 additions & 1 deletion app/src/epic.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { networkingEpic } from './networking/epic'
import { shellEpic } from './shell/epic'
import { alertsEpic } from './alerts/epic'
import { systemInfoEpic } from './system-info/epic'
import { sessionsEpic } from './sessions/epic'

import type { Epic } from './types'

Expand All @@ -33,5 +34,6 @@ export const rootEpic: Epic = combineEpics(
networkingEpic,
shellEpic,
alertsEpic,
systemInfoEpic
systemInfoEpic,
sessionsEpic
)
3 changes: 3 additions & 0 deletions app/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import { systemInfoReducer } from './system-info/reducer'
// app-wide alerts state
import { alertsReducer } from './alerts/reducer'

import { robotSessionReducer } from './sessions/reducer'

import type { Reducer } from 'redux'
import type { State, Action } from './types'

Expand All @@ -83,5 +85,6 @@ export const rootReducer: Reducer<State, Action> = combineReducers<_, Action>({
shell: shellReducer,
systemInfo: systemInfoReducer,
alerts: alertsReducer,
sessions: robotSessionReducer,
router: connectRouter<_, Action>(history),
})
10 changes: 10 additions & 0 deletions app/src/robot-api/__fixtures__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
RobotHost,
RobotApiRequestMeta,
RequestState,
RobotApiV2Error,
RobotApiV2ErrorResponseBody,
} from '../types'

export type ResponseFixturesOptions<SuccessBody, FailureBody> = {|
Expand Down Expand Up @@ -78,3 +80,11 @@ export const makeResponseFixtures = <SuccessBody, FailureBody>(

return { successMeta, success, failureMeta, failure }
}

export const mockV2Error: RobotApiV2Error = {
status: 'went bad',
}

export const mockV2ErrorResponse: RobotApiV2ErrorResponseBody = {
errors: [mockV2Error],
}
46 changes: 46 additions & 0 deletions app/src/robot-api/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,49 @@ export type RobotApiState = $Shape<
[requestId: string]: void | RequestState,
|}>
>

export type ResourceLink = {|
href: string,
meta?: $Shape<{| [string]: string | void |}>,
|}

export type ResourceLinks = $Shape<{| [string]: ResourceLink | string | void |}>

// generic response data supertype
export type RobotApiV2ResponseData = {|
id: string,
// the "+" means that these properties are covariant, so
// "extending" types may specify more strict subtypes
+type: string,
+attributes: { ... },
|}

// parameterized response type
// DataT parameter must be a subtype of RobotApiV2ResponseData
// MetaT defaults to void if unspecified
export type RobotApiV2ResponseBody<
DataT: RobotApiV2ResponseData,
MetaT = void
> = {|
data: DataT,
links?: ResourceLinks,
meta?: MetaT,
|}

export type RobotApiV2Error = {|
id?: string,
links?: ResourceLinks,
status?: string,
code?: string,
title?: string,
detail?: string,
source?: {|
pointer?: string,
parameter?: string,
|},
meta?: { ... },
|}

export type RobotApiV2ErrorResponseBody = {
errors: Array<RobotApiV2Error>,
}
109 changes: 109 additions & 0 deletions app/src/sessions/__fixtures__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// @flow

import * as Types from '../types'
import * as Constants from '../constants'
import { POST, DELETE, GET } from '../../robot-api'
import {
makeResponseFixtures,
mockV2ErrorResponse,
} from '../../robot-api/__fixtures__'

import type { RobotApiV2ErrorResponseBody } from '../../robot-api/types'

export const mockSessionData: Types.Session = {
sessionType: 'calibrationCheck',
details: { someData: 5 },
}

export const mockSessionCommand: Types.SessionCommand = {
command: 'dosomething',
data: { someData: 32 },
}

export const mockSessionCommandData: Types.SessionCommand = {
command: '4321',
status: 'accepted',
data: {},
}

export const mockSessionResponse: Types.SessionResponse = {
data: {
id: '1234',
type: 'Session',
attributes: mockSessionData,
},
}

export const mockSessionCommandResponse: Types.SessionCommandResponse = {
data: {
id: '4321',
type: 'SessionCommand',
attributes: mockSessionCommandData,
},
meta: {
sessionType: 'calibrationCheck',
details: {
someData: 15,
someOtherData: 'hi',
},
},
}

export const {
successMeta: mockCreateSessionSuccessMeta,
failureMeta: mockCreateSessionFailureMeta,
success: mockCreateSessionSuccess,
failure: mockCreateSessionFailure,
} = makeResponseFixtures<Types.SessionResponse, RobotApiV2ErrorResponseBody>({
method: POST,
path: Constants.SESSIONS_PATH,
successStatus: 201,
successBody: mockSessionResponse,
failureStatus: 500,
failureBody: mockV2ErrorResponse,
})

export const {
successMeta: mockDeleteSessionSuccessMeta,
failureMeta: mockDeleteSessionFailureMeta,
success: mockDeleteSessionSuccess,
failure: mockDeleteSessionFailure,
} = makeResponseFixtures<Types.SessionResponse, RobotApiV2ErrorResponseBody>({
method: DELETE,
path: `${Constants.SESSIONS_PATH}/1234`,
successStatus: 200,
successBody: mockSessionResponse,
failureStatus: 500,
failureBody: mockV2ErrorResponse,
})

export const {
successMeta: mockFetchSessionSuccessMeta,
failureMeta: mockFetchSessionFailureMeta,
success: mockFetchSessionSuccess,
failure: mockFetchSessionFailure,
} = makeResponseFixtures<Types.SessionResponse, RobotApiV2ErrorResponseBody>({
method: GET,
path: `${Constants.SESSIONS_PATH}/1234`,
successStatus: 200,
successBody: mockSessionResponse,
failureStatus: 500,
failureBody: mockV2ErrorResponse,
})

export const {
successMeta: mockSessionCommandsSuccessMeta,
failureMeta: mockSessionCommandsFailureMeta,
success: mockSessionCommandsSuccess,
failure: mockSessionCommandsFailure,
} = makeResponseFixtures<
Types.SessionCommandResponse,
RobotApiV2ErrorResponseBody
>({
method: GET,
path: `${Constants.SESSIONS_PATH}/1234/${Constants.SESSIONS_COMMANDS_PATH_EXTENSION}`,
successStatus: 200,
successBody: mockSessionCommandResponse,
failureStatus: 500,
failureBody: mockV2ErrorResponse,
})
171 changes: 171 additions & 0 deletions app/src/sessions/__tests__/actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// @flow

import * as Actions from '../actions'
import * as Fixtures from '../__fixtures__'

import type { SessionsAction } from '../types'

import { mockV2ErrorResponse } from '../../robot-api/__fixtures__'

type ActionSpec = {|
name: string,
creator: (...Array<any>) => mixed,
args: Array<mixed>,
expected: SessionsAction,
|}

describe('robot session check actions', () => {
const SPECS: Array<ActionSpec> = [
{
name: 'sessions:CREATE_SESSION',
creator: Actions.createSession,
args: ['robot-name', 'calibrationCheck'],
expected: {
type: 'sessions:CREATE_SESSION',
payload: { robotName: 'robot-name', sessionType: 'calibrationCheck' },
meta: {},
},
},
{
name: 'sessions:CREATE_SESSION_SUCCESS',
creator: Actions.createSessionSuccess,
args: ['robot-name', Fixtures.mockSessionResponse, { requestId: 'abc' }],
expected: {
type: 'sessions:CREATE_SESSION_SUCCESS',
payload: {
robotName: 'robot-name',
...Fixtures.mockSessionResponse,
},
meta: { requestId: 'abc' },
},
},
{
name: 'sessions:CREATE_SESSION_FAILURE',
creator: Actions.createSessionFailure,
args: ['robot-name', mockV2ErrorResponse, { requestId: 'abc' }],
expected: {
type: 'sessions:CREATE_SESSION_FAILURE',
payload: { robotName: 'robot-name', error: mockV2ErrorResponse },
meta: { requestId: 'abc' },
},
},
{
name: 'sessions:DELETE_SESSION',
creator: Actions.deleteSession,
args: ['robot-name', '1234'],
expected: {
type: 'sessions:DELETE_SESSION',
payload: { robotName: 'robot-name', sessionId: '1234' },
meta: {},
},
},
{
name: 'sessions:DELETE_SESSION_SUCCESS',
creator: Actions.deleteSessionSuccess,
args: ['robot-name', Fixtures.mockSessionResponse, { requestId: 'abc' }],
expected: {
type: 'sessions:DELETE_SESSION_SUCCESS',
payload: {
robotName: 'robot-name',
...Fixtures.mockSessionResponse,
},
meta: { requestId: 'abc' },
},
},
{
name: 'sessions:DELETE_SESSION_FAILURE',
creator: Actions.deleteSessionFailure,
args: ['robot-name', mockV2ErrorResponse, { requestId: 'abc' }],
expected: {
type: 'sessions:DELETE_SESSION_FAILURE',
payload: { robotName: 'robot-name', error: mockV2ErrorResponse },
meta: { requestId: 'abc' },
},
},
{
name: 'sessions:FETCH_SESSION',
creator: Actions.fetchSession,
args: ['robot-name', '1234'],
expected: {
type: 'sessions:FETCH_SESSION',
payload: { robotName: 'robot-name', sessionId: '1234' },
meta: {},
},
},
{
name: 'sessions:FETCH_SESSION_SUCCESS',
creator: Actions.fetchSessionSuccess,
args: ['robot-name', Fixtures.mockSessionResponse, { requestId: 'abc' }],
expected: {
type: 'sessions:FETCH_SESSION_SUCCESS',
payload: {
robotName: 'robot-name',
...Fixtures.mockSessionResponse,
},
meta: { requestId: 'abc' },
},
},
{
name: 'sessions:FETCH_SESSION_FAILURE',
creator: Actions.fetchSessionFailure,
args: ['robot-name', mockV2ErrorResponse, { requestId: 'abc' }],
expected: {
type: 'sessions:FETCH_SESSION_FAILURE',
payload: { robotName: 'robot-name', error: mockV2ErrorResponse },
meta: { requestId: 'abc' },
},
},
{
name: 'sessions:CREATE_SESSION_COMMAND',
creator: Actions.createSessionCommand,
args: ['robot-name', '1234', Fixtures.mockSessionCommand],
expected: {
type: 'sessions:CREATE_SESSION_COMMAND',
payload: {
robotName: 'robot-name',
sessionId: '1234',
command: Fixtures.mockSessionCommand,
},
meta: {},
},
},
{
name: 'sessions:CREATE_SESSION_COMMAND_SUCCESS',
creator: Actions.createSessionCommandSuccess,
args: [
'robot-name',
'1234',
Fixtures.mockSessionCommandResponse,
{ requestId: 'abc' },
],
expected: {
type: 'sessions:CREATE_SESSION_COMMAND_SUCCESS',
payload: {
robotName: 'robot-name',
sessionId: '1234',
...Fixtures.mockSessionCommandResponse,
},
meta: { requestId: 'abc' },
},
},
{
name: 'sessions:CREATE_SESSION_COMMAND_FAILURE',
creator: Actions.createSessionCommandFailure,
args: ['robot-name', '1234', mockV2ErrorResponse, { requestId: 'abc' }],
expected: {
type: 'sessions:CREATE_SESSION_COMMAND_FAILURE',
payload: {
robotName: 'robot-name',
sessionId: '1234',
error: mockV2ErrorResponse,
},
meta: { requestId: 'abc' },
},
},
]

SPECS.forEach(spec => {
const { name, creator, args, expected } = spec
it(name, () => expect(creator(...args)).toEqual(expected))
})
})
Loading

0 comments on commit 441d682

Please sign in to comment.