Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions packages/backend/src/graphql/resolvers/tenant_settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ import {
createHttpLink,
ApolloLink,
InMemoryCache,
gql
gql,
ApolloError
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { TenantSettingKeys } from '../../tenants/settings/model'
import { faker } from '@faker-js/faker'
import {
errorToCode,
errorToMessage,
TenantSettingError
} from '../../tenants/settings/errors'

function createTenantedApolloClient(
appContainer: TestContainer,
Expand Down Expand Up @@ -93,7 +100,10 @@ describe('Tenant Settings Resolvers', (): void => {
test('can create tenant setting', async (): Promise<void> => {
const input: CreateTenantSettingsInput = {
settings: [
{ key: TenantSettingKeys.EXCHANGE_RATES_URL.name, value: 'MY_VALUE' }
{
key: TenantSettingKeys.EXCHANGE_RATES_URL.name,
value: faker.internet.url()
}
]
}

Expand Down Expand Up @@ -122,6 +132,56 @@ describe('Tenant Settings Resolvers', (): void => {

expect(response.settings.length).toBeGreaterThan(0)
})

test('errors when invalid input is provided', async (): Promise<void> => {
const input: CreateTenantSettingsInput = {
settings: [
{
key: TenantSettingKeys.WEBHOOK_MAX_RETRY.name,
value: '-1'
}
]
}

const tenant = await createTenant(deps)
const client = createTenantedApolloClient(appContainer, tenant.id)
expect.assertions(2)

try {
await client
.mutate({
mutation: gql`
mutation CreateTenantSettings(
$input: CreateTenantSettingsInput!
) {
createTenantSettings(input: $input) {
settings {
key
value
}
}
}
`,
variables: { input }
})
.then((query): CreateTenantSettingsMutationResponse => {
if (query.data) {
return query.data.createTenantSettings
}
throw new Error('Data was empty')
})
} catch (error) {
expect(error).toBeInstanceOf(ApolloError)
expect((error as ApolloError).graphQLErrors).toContainEqual(
expect.objectContaining({
message: errorToMessage[TenantSettingError.InvalidSetting],
extensions: expect.objectContaining({
code: errorToCode[TenantSettingError.InvalidSetting]
})
})
)
}
})
})

describe('Get Tenant Settings', (): void => {
Expand Down
18 changes: 16 additions & 2 deletions packages/backend/src/graphql/resolvers/tenant_settings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { GraphQLError } from 'graphql'
import { TenantedApolloContext } from '../../app'
import {
isTenantSettingError,
errorToCode,
errorToMessage
} from '../../tenants/settings/errors'
import { TenantSetting } from '../../tenants/settings/model'
import {
ResolversTypes,
Expand Down Expand Up @@ -32,13 +38,21 @@ export const createTenantSettings: MutationResolvers<TenantedApolloContext>['cre
): Promise<ResolversTypes['CreateTenantSettingsMutationResponse']> => {
const tenantSettingService = await ctx.container.use('tenantSettingService')

const tenantSettings = await tenantSettingService.create({
const tenantSettingsOrError = await tenantSettingService.create({
tenantId: ctx.tenant.id,
setting: args.input.settings
})

if (isTenantSettingError(tenantSettingsOrError)) {
throw new GraphQLError(errorToMessage[tenantSettingsOrError], {
extensions: {
code: errorToCode[tenantSettingsOrError]
}
})
}

return {
settings: tenantSettingsToGraphql(tenantSettings)
settings: tenantSettingsToGraphql(tenantSettingsOrError)
}
}

Expand Down
8 changes: 6 additions & 2 deletions packages/backend/src/tenants/settings/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { GraphQLErrorCode } from '../../graphql/errors'

export enum TenantSettingError {
TenantNotFound = 'TenantNotFound',
UnknownError = 'UnknownError'
UnknownError = 'UnknownError',
InvalidSetting = 'InvalidSettingError'
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -12,6 +13,7 @@ export const isTenantSettingError = (t: any): t is TenantSettingError =>
export const errorToCode: {
[key in TenantSettingError]: GraphQLErrorCode
} = {
[TenantSettingError.InvalidSetting]: GraphQLErrorCode.BadUserInput,
[TenantSettingError.TenantNotFound]: GraphQLErrorCode.NotFound,
[TenantSettingError.UnknownError]: GraphQLErrorCode.InternalServerError
}
Expand All @@ -20,5 +22,7 @@ export const errorToMessage: {
[key in TenantSettingError]: string
} = {
[TenantSettingError.TenantNotFound]: 'Tenant not found',
[TenantSettingError.UnknownError]: 'Unknown error'
[TenantSettingError.UnknownError]: 'Unknown error',
[TenantSettingError.InvalidSetting]:
'Invalid value for one or more tenant settings'
}
27 changes: 26 additions & 1 deletion packages/backend/src/tenants/settings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ const TENANT_KEY_MAPPING = {
[TenantSettingKeys.EXCHANGE_RATES_URL.name]: 'exchangeRatesUrl',
[TenantSettingKeys.WEBHOOK_MAX_RETRY.name]: 'webhookMaxRetry',
[TenantSettingKeys.WEBHOOK_TIMEOUT.name]: 'webhookTimeout',
[TenantSettingKeys.WEBHOOK_URL.name]: 'webhookUrl'
[TenantSettingKeys.WEBHOOK_URL.name]: 'webhookUrl',
[TenantSettingKeys.WALLET_ADDRESS_URL.name]: 'walletAddressUrl'
} as const

export type FormattedTenantSettings = Record<
Expand All @@ -74,3 +75,27 @@ export const formatSettings = (
}
return settingsObj
}

const validateUrlTenantSetting = (url: string): boolean => {
try {
return !!new URL(url)
} catch (err) {
return false
}
}

const validateNonNegativeTenantSetting = (numberString: string): boolean => {
return !!(Number.isFinite(Number(numberString)) && Number(numberString) > -1)
}

const validatePositiveTenantSetting = (numberString: string): boolean => {
return !!(Number.isFinite(Number(numberString)) && Number(numberString) > 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can have a setting that allows 0?
For example, WEBHOOK_MAX_RETRY seems like it should be able to accept 0

}

export const TENANT_SETTING_VALIDATORS = {
[TenantSettingKeys.EXCHANGE_RATES_URL.name]: validateUrlTenantSetting,
[TenantSettingKeys.WEBHOOK_MAX_RETRY.name]: validateNonNegativeTenantSetting,
[TenantSettingKeys.WEBHOOK_TIMEOUT.name]: validatePositiveTenantSetting,
[TenantSettingKeys.WEBHOOK_URL.name]: validateUrlTenantSetting,
[TenantSettingKeys.WALLET_ADDRESS_URL.name]: validateUrlTenantSetting
}
Loading
Loading