Skip to content

Commit

Permalink
fix: add "ctx.field()" for GraphQL responses (#1257)
Browse files Browse the repository at this point in the history
* feat: add field() method to graphQL response context

fix #1200

* style(src/context/field.ts): rename assertFieldNameIsValid to validateFieldName

* style(src/handlers/graphqlhandler.ts): use shorthand assignemnt

* refactor(ctx.field()): update filename validation exceptions, correct forbiden names values

* test(field): expand error unit tests

* chore(field): add whitespace between name validations

Co-authored-by: Artem Zakharchenko <kettanaito@gmail.com>
  • Loading branch information
Poivey and kettanaito committed Jun 7, 2022
1 parent 1b9dde0 commit 442f48d
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 0 deletions.
117 changes: 117 additions & 0 deletions src/context/field.test.ts
@@ -0,0 +1,117 @@
/**
* @jest-environment jsdom
*/
import { field } from './field'
import { response } from '../response'
import { data } from './data'
import { errors } from './errors'

test('sets a given field value string on the response JSON body', async () => {
const result = await response(field('field', 'value'))

expect(result.headers.get('content-type')).toBe('application/json')
expect(result).toHaveProperty('body', JSON.stringify({ field: 'value' }))
})

test('sets a given field value object on the response JSON body', async () => {
const result = await response(
field('metadata', {
date: new Date('2022-05-27'),
comment: 'nice metadata',
}),
)

expect(result.headers.get('content-type')).toBe('application/json')
expect(result).toHaveProperty(
'body',
JSON.stringify({
metadata: { date: new Date('2022-05-27'), comment: 'nice metadata' },
}),
)
})

test('combines with data, errors and other field in the response JSON body', async () => {
const result = await response(
data({ name: 'msw' }),
errors([{ message: 'exceeds the limit of awesomeness' }]),
field('field', { errorCode: 'value' }),
field('field2', 123),
)

expect(result.headers.get('content-type')).toEqual('application/json')
expect(result).toHaveProperty(
'body',
JSON.stringify({
field2: 123,
field: { errorCode: 'value' },
errors: [
{
message: 'exceeds the limit of awesomeness',
},
],
data: {
name: 'msw',
},
}),
)
})

test('throws when trying to set non-serializable values', async () => {
await expect(response(field('metadata', BigInt(1)))).rejects.toThrow(
'Do not know how to serialize a BigInt',
)
})

test('throws when passing an empty string as field name', async () => {
await expect(response(field('' as string, 'value'))).rejects.toThrow(
`[MSW] Failed to set a custom field on a GraphQL response: field name cannot be empty.`,
)
})

test('throws when passing an empty string (when trimmed) as field name', async () => {
await expect(response(field(' ' as string, 'value'))).rejects.toThrow(
`[MSW] Failed to set a custom field on a GraphQL response: field name cannot be empty.`,
)
})

test('throws when using "data" as the field name', async () => {
await expect(
response(
field(
// @ts-expect-error Test runtime value.
'data',
'value',
),
),
).rejects.toThrow(
'[MSW] Failed to set a custom "data" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.data()" instead?',
)
})

test('throws when using "errors" as the field name', async () => {
await expect(
response(
field(
// @ts-expect-error Test runtime value.
'errors',
'value',
),
),
).rejects.toThrow(
'[MSW] Failed to set a custom "errors" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.errors()" instead?',
)
})

test('throws when using "extensions" as the field name', async () => {
await expect(
response(
field(
// @ts-expect-error Test runtime value.
'extensions',
'value',
),
),
).rejects.toThrow(
'[MSW] Failed to set a custom "extensions" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.extensions()" instead?',
)
})
60 changes: 60 additions & 0 deletions src/context/field.ts
@@ -0,0 +1,60 @@
import { invariant } from 'outvariant'
import { ResponseTransformer } from '../response'
import { devUtils } from '../utils/internal/devUtils'
import { jsonParse } from '../utils/internal/jsonParse'
import { mergeRight } from '../utils/internal/mergeRight'
import { json } from './json'

type ForbiddenFieldNames = '' | 'data' | 'errors' | 'extensions'

/**
* Set a custom field on the GraphQL mocked response.
* @example res(ctx.fields('customField', value))
* @see {@link https://mswjs.io/docs/api/context/field}
*/
export const field = <FieldNameType extends string, FieldValueType>(
fieldName: FieldNameType extends ForbiddenFieldNames ? never : FieldNameType,
fieldValue: FieldValueType,
): ResponseTransformer<string> => {
return (res) => {
validateFieldName(fieldName)

const prevBody = jsonParse(res.body) || {}
const nextBody = mergeRight(prevBody, { [fieldName]: fieldValue })

return json(nextBody)(res as any) as any
}
}

function validateFieldName(fieldName: string) {
invariant(
fieldName.trim() !== '',
devUtils.formatMessage(
'Failed to set a custom field on a GraphQL response: field name cannot be empty.',
),
)

invariant(
fieldName !== 'data',
devUtils.formatMessage(
'Failed to set a custom "%s" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.data()" instead?',
fieldName,
),
)

invariant(
fieldName !== 'errors',
devUtils.formatMessage(
'Failed to set a custom "%s" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.errors()" instead?',
fieldName,
),
)

invariant(
fieldName !== 'extensions',
devUtils.formatMessage(
'Failed to set a custom "%s" field on a mocked GraphQL response: forbidden field name. Did you mean to call "ctx.extensions()" instead?',
fieldName,
),
)
}
3 changes: 3 additions & 0 deletions src/handlers/GraphQLHandler.ts
Expand Up @@ -3,6 +3,7 @@ import { SerializedResponse } from '../setupWorker/glossary'
import { data } from '../context/data'
import { extensions } from '../context/extensions'
import { errors } from '../context/errors'
import { field } from '../context/field'
import { GraphQLPayloadContext } from '../typeUtils'
import { cookie } from '../context/cookie'
import {
Expand Down Expand Up @@ -40,6 +41,7 @@ export type GraphQLContext<QueryType extends Record<string, unknown>> =
extensions: GraphQLPayloadContext<QueryType>
errors: typeof errors
cookie: typeof cookie
field: typeof field
}

export const graphqlContext: GraphQLContext<any> = {
Expand All @@ -48,6 +50,7 @@ export const graphqlContext: GraphQLContext<any> = {
extensions,
errors,
cookie,
field,
}

export type GraphQLVariables = Record<string, any>
Expand Down

0 comments on commit 442f48d

Please sign in to comment.