Skip to content

Commit

Permalink
Maintenance: Validate GraphQL operation variables in tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored and dominikklein committed Feb 8, 2024
1 parent 7c08ed8 commit c55d180
Show file tree
Hide file tree
Showing 10 changed files with 482 additions and 52 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Skip auto generated files.
app/frontend/**/graphql/**/*.ts
!app/frontend/tests/graphql/**/*.ts
app/frontend/shared/graphql/types.ts
app/frontend/shared/types/config.ts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ const renderTwoFactor = async (twoFactor: TwoFactorPlugin) => {
return view
}

const securitySuccess = () => {
return {
success: true,
payload: {
challenge: 'publicKey.challenge',
credential: 'publicKeyCredential',
},
}
}

describe('non-form two factor', () => {
mockApplicationConfig({})

Expand All @@ -65,7 +75,7 @@ describe('non-form two factor', () => {

const view = await renderTwoFactor({
...SecurityKeys,
setup: () => Promise.resolve({ success: true }),
setup: () => Promise.resolve(securitySuccess()),
})

await expect(view.findByText(error)).resolves.toBeInTheDocument()
Expand All @@ -76,7 +86,7 @@ describe('non-form two factor', () => {

const view = await renderTwoFactor({
...SecurityKeys,
setup: () => Promise.resolve({ success: true }),
setup: () => Promise.resolve(securitySuccess()),
})

await expect(
Expand All @@ -94,7 +104,7 @@ describe('non-form two factor', () => {

const view = await renderTwoFactor({
...SecurityKeys,
setup: () => Promise.resolve({ success: false, error }),
setup: () => Promise.resolve({ success: false, retry: false, error }),
})

await expect(view.findByText(error)).resolves.toBeInTheDocument()
Expand All @@ -112,7 +122,7 @@ describe('non-form two factor', () => {
})

await expect(view.findByRole('status')).resolves.toBeInTheDocument()
resolve({ success: true })
resolve(securitySuccess())

await waitFor(() => {
expect(view.queryByRole('status')).not.toBeInTheDocument()
Expand Down Expand Up @@ -153,7 +163,7 @@ describe('non-form two factor', () => {

const view = await renderTwoFactor({
...SecurityKeys,
setup: () => Promise.resolve({ success: false, error }),
setup: () => Promise.resolve({ success: false, retry: true, error }),
})

await expect(view.findByText(error)).resolves.toBeInTheDocument()
Expand All @@ -168,7 +178,9 @@ describe('non-form two factor', () => {
initiationData: { foo: 'bar' },
})

const setup = vi.fn().mockResolvedValue({ success: false, error })
const setup = vi
.fn()
.mockResolvedValue({ success: false, retry: true, error })

const view = await renderTwoFactor({
...SecurityKeys,
Expand All @@ -177,7 +189,11 @@ describe('non-form two factor', () => {

await expect(view.findByText(error)).resolves.toBeInTheDocument()

setup.mockResolvedValueOnce({ success: false, error: 'New Error!' })
setup.mockResolvedValueOnce({
success: false,
retry: true,
error: 'New Error!',
})

await view.events.click(view.getByRole('button', { name: 'Retry' }))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,6 @@ describe('guided setup manual email notification', () => {
})

it('can save and continue to channels step', async () => {
const view = await visitView('/guided-setup/manual/email-notification')

await flushPromises()
await getNode('email-notification-setup')?.settled

expect(view.getByText('Email Notification')).toBeInTheDocument()
expect(view.getByLabelText('Send Mails via')).toBeInTheDocument()

mockChannelEmailValidateConfigurationOutboundMutation({
channelEmailValidateConfigurationOutbound: {
success: true,
Expand All @@ -97,6 +89,14 @@ describe('guided setup manual email notification', () => {
},
})

const view = await visitView('/guided-setup/manual/email-notification')

await flushPromises()
await getNode('email-notification-setup')?.settled

expect(view.getByText('Email Notification')).toBeInTheDocument()
expect(view.getByLabelText('Send Mails via')).toBeInTheDocument()

const continueButton = view.getByRole('button', {
name: 'Save and Continue',
})
Expand Down Expand Up @@ -191,6 +191,19 @@ describe('guided setup manual email notification', () => {
})

it('submits `false` value when SSL verification is disabled', async () => {
mockChannelEmailValidateConfigurationOutboundMutation({
channelEmailValidateConfigurationOutbound: {
success: true,
errors: null,
},
})

mockChannelEmailSetNotificationConfigurationMutation({
channelEmailSetNotificationConfiguration: {
success: true,
},
})

const view = await visitView('/guided-setup/manual/email-notification')

await flushPromises()
Expand All @@ -214,19 +227,6 @@ describe('guided setup manual email notification', () => {
getNode('email-notification-setup')?.find('sslVerify')?.value,
).toBe(true)

mockChannelEmailValidateConfigurationOutboundMutation({
channelEmailValidateConfigurationOutbound: {
success: true,
errors: null,
},
})

mockChannelEmailSetNotificationConfigurationMutation({
channelEmailSetNotificationConfiguration: {
success: true,
},
})

await view.events.click(
view.getByRole('button', {
name: 'Save and Continue',
Expand Down
147 changes: 137 additions & 10 deletions app/frontend/tests/graphql/builders/__tests__/mutation-calls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import type { MutationsAccountAvatarAddArgs } from '#shared/graphql/types.ts'
import { faker } from '@faker-js/faker'
import { convertToGraphQLId } from '#shared/graphql/utils.ts'
import UserError from '#shared/errors/UserError.ts'
import { LoginDocument } from '#shared/graphql/mutations/login.api.ts'
import { getGraphQLMockCalls, mockGraphQLResult } from '../mocks.ts'
import { getMutationHandler, getQueryHandler } from './utils.ts'
import {
type TestAvatarMutation,
TestAvatarActiveMutationDocument,
TestUserAutorizationsDocument,
type TestUserAuthorizationsMutation,
type TestUserAuthorizationsVariables,
TestUserUpdateDocument,
type TestUserUpdateMutation,
type TestUserUpdateVariables,
TestUserDocument,
type TestUserQuery,
type TestUserQueryVariables,
Expand Down Expand Up @@ -48,12 +49,12 @@ describe('calling mutation without mocking document works correctly', () => {
})

it('mutation correctly processed data with arrays', async () => {
expect(getGraphQLMockCalls(TestUserAutorizationsDocument)).toHaveLength(0)
expect(getGraphQLMockCalls(TestUserUpdateDocument)).toHaveLength(0)

const handler = getMutationHandler<
TestUserAuthorizationsMutation,
TestUserAuthorizationsVariables
>(TestUserAutorizationsDocument)
TestUserUpdateMutation,
TestUserUpdateVariables
>(TestUserUpdateDocument)

const userId = convertToGraphQLId('User', 42)

Expand Down Expand Up @@ -100,9 +101,9 @@ describe('calling mutation without mocking document works correctly', () => {
})

const mutationHandler = getMutationHandler<
TestUserAuthorizationsMutation,
TestUserAuthorizationsVariables
>(TestUserAutorizationsDocument)
TestUserUpdateMutation,
TestUserUpdateVariables
>(TestUserUpdateDocument)

const mutationData = await mutationHandler.send({
userId,
Expand Down Expand Up @@ -214,4 +215,130 @@ describe('calling mutation with mocked return data correctly returns data', () =
expect(data?.userSignup.success).toBe(true)
expect(data?.userSignup.errors).toBeNull()
})

describe('correctly validates variables', () => {
it('throws an error if field is required, but not defined', async () => {
const handler = getMutationHandler(TestUserSignupMutationDocument)

await expect(() =>
handler.send({}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation userSignup) non-nullable field "input" is not defined]`,
)
})

it('throws an error if field is required inside the list, but not defined', async () => {
const mutationHandler = getMutationHandler(TestUserUpdateDocument)

await expect(() =>
mutationHandler.send({
userId: '1',
input: {
objectAttributeValues: [{}],
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation userUpdate) non-nullable field "name" is not defined]`,
)
})

it('throws an error if field is not defined on the inner type', async () => {
const handler = getMutationHandler(TestUserSignupMutationDocument)

await expect(() =>
handler.send({
input: { email: 'email', password: 'password', invalidField: true },
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation userSignup) field "invalidField" is not defined on UserSignupInput]`,
)
})

it('throws an error if field is not defined on the outer type', async () => {
const handler = getMutationHandler(TestUserSignupMutationDocument)

await expect(() =>
handler.send({
invalidField: true,
input: { email: 'email', password: 'password' },
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation userSignup) field "invalidField" is not defined on mutation userSignup]`,
)
})

it('throws an error if field is not defined on the type inside the list', async () => {
const mutationHandler = getMutationHandler(TestUserUpdateDocument)

await expect(() =>
mutationHandler.send({
userId: '1',
input: {
objectAttributeValues: [{ invalidField: true }],
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation userUpdate) field "invalidField" is not defined on ObjectAttributeValueInput]`,
)
})

it('throws an error if field is not the correct scalar type', async () => {
const handler = getMutationHandler(TestUserSignupMutationDocument)

await expect(() =>
handler.send({
input: { email: 123, password: 'password' },
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation userSignup) expected string for "email", got number]`,
)
})

it('throws an error if field is not the correct object type', async () => {
const handler = getMutationHandler(TestUserSignupMutationDocument)

await expect(() =>
handler.send({
input: 123,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation userSignup) expected object for "input", got number]`,
)
})

it('throws an error if field is not the correct type inside the list', async () => {
const mutationHandler = getMutationHandler(TestUserUpdateDocument)

await expect(() =>
mutationHandler.send({
userId: '1',
input: {
objectAttributeValues: [{ name: 123 }],
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation userUpdate) expected string for "name", got number]`,
)
})

it('throws an error if field is not in enum', async () => {
const mutationHandler = getMutationHandler(LoginDocument)

await expect(() =>
mutationHandler.send({
input: {
login: 'login',
password: 'password',
rememberMe: true,
twoFactorAuthentication: {
twoFactorMethod: 'unknown_method_to_test_error',
twoFactorPayload: 'some_payload',
},
},
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[ApolloError: (Variables error for mutation login) twoFactorMethod should be one of "authenticator_app", "security_keys", but instead got "unknown_method_to_test_error"]`,
)
})
})
})
10 changes: 7 additions & 3 deletions app/frontend/tests/graphql/builders/__tests__/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface TestUserQuery {
}
}

export interface TestUserAuthorizationsMutation {
export interface TestUserUpdateMutation {
userUpdate: {
user: {
id: string
Expand All @@ -36,7 +36,7 @@ export interface TestUserAuthorizationsMutation {
}
}

export interface TestUserAuthorizationsVariables {
export interface TestUserUpdateVariables {
userId: string
input: UserInput
}
Expand Down Expand Up @@ -122,7 +122,7 @@ export const TestUserDocument = gql`
}
`

export const TestUserAutorizationsDocument = gql`
export const TestUserUpdateDocument = gql`
mutation userUpdate($userId: ID, $input: UserInput!) {
userUpdate(id: $userId, input: $input) {
user {
Expand Down Expand Up @@ -171,6 +171,10 @@ export interface TestUserUpdatesSubscription {
}
}

export interface TestUserUpdatesSubscriptionVariables {
userId: string
}

export const TestUserUpdatesDocument = gql`
subscription userUpdates($userId: ID!) {
userUpdates(userId: $userId) {
Expand Down
Loading

0 comments on commit c55d180

Please sign in to comment.