Skip to content

Commit

Permalink
fix(resolver): update unstoppable supported tlds and manage errors (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
banklesss authored and SorinC6 committed Jan 23, 2024
1 parent cc849ab commit 79a1b60
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 48 deletions.
16 changes: 13 additions & 3 deletions packages/common/src/api/fetchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,29 @@ export const fetchData: FetchData = <T, D = any>(
if (error.response) {
const status = error.response.status
const message = error.response.statusText
const responseData = error.response.data

return {
tag: 'left',
error: {status, message},
error: {status, message, responseData},
} as const
} else if (error.request) {
return {
tag: 'left',
error: {status: -1, message: 'Network (no response)'},
error: {
status: -1,
message: 'Network (no response)',
responseData: null,
},
} as const
} else {
return {
tag: 'left',
error: {status: -2, message: `Invalid state: ${error.message}`},
error: {
status: -2,
message: `Invalid state: ${error.message}`,
responseData: null,
},
} as const
}
})
Expand Down
84 changes: 58 additions & 26 deletions packages/common/src/api/handleApiError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,114 @@ import {Api} from '@yoroi/types'
describe('handleApiError', () => {
it('should throw NetworkError for -1 status', () => {
expect(() =>
handleApiError({status: -1, message: 'Network error'}),
handleApiError({
status: -1,
message: 'Network error',
responseData: null,
}),
).toThrow(Api.Errors.Network)
})

it('should throw InvalidStateError for -2 status', () => {
expect(() =>
handleApiError({status: -2, message: 'Invalid state'}),
handleApiError({
status: -2,
message: 'Invalid state',
responseData: null,
}),
).toThrow(Api.Errors.InvalidState)
})

it('should throw BadRequestError for 400 status', () => {
expect(() => handleApiError({status: 400, message: 'Bad request'})).toThrow(
Api.Errors.BadRequest,
)
expect(() =>
handleApiError({status: 400, message: 'Bad request', responseData: null}),
).toThrow(Api.Errors.BadRequest)
})

it('should throw UnauthorizedError for 401 status', () => {
expect(() =>
handleApiError({status: 401, message: 'Unauthorized'}),
handleApiError({
status: 401,
message: 'Unauthorized',
responseData: null,
}),
).toThrow(Api.Errors.Unauthorized)
})

it('should throw ForbiddenError for 403 status', () => {
expect(() => handleApiError({status: 403, message: 'Forbidden'})).toThrow(
Api.Errors.Forbidden,
)
expect(() =>
handleApiError({status: 403, message: 'Forbidden', responseData: null}),
).toThrow(Api.Errors.Forbidden)
})

it('should throw NotFoundError for 404 status', () => {
expect(() => handleApiError({status: 404, message: 'Not found'})).toThrow(
Api.Errors.NotFound,
)
expect(() =>
handleApiError({status: 404, message: 'Not found', responseData: null}),
).toThrow(Api.Errors.NotFound)
})

it('should throw ConflictError for 409 status', () => {
expect(() => handleApiError({status: 409, message: 'Conflict'})).toThrow(
Api.Errors.Conflict,
)
expect(() =>
handleApiError({status: 409, message: 'Conflict', responseData: null}),
).toThrow(Api.Errors.Conflict)
})

it('should throw GoneError for 410 status', () => {
expect(() => handleApiError({status: 410, message: 'Gone'})).toThrow(
Api.Errors.Gone,
)
expect(() =>
handleApiError({status: 410, message: 'Gone', responseData: null}),
).toThrow(Api.Errors.Gone)
})

it('should throw TooEarlyError for 425 status', () => {
expect(() => handleApiError({status: 425, message: 'Too early'})).toThrow(
Api.Errors.TooEarly,
)
expect(() =>
handleApiError({status: 425, message: 'Too early', responseData: null}),
).toThrow(Api.Errors.TooEarly)
})

it('should throw TooManyRequestsError for 429 status', () => {
expect(() =>
handleApiError({status: 429, message: 'Too many requests'}),
handleApiError({
status: 429,
message: 'Too many requests',
responseData: null,
}),
).toThrow(Api.Errors.TooManyRequests)
})

it('should throw ServerSideError for 500 status', () => {
expect(() =>
handleApiError({status: 500, message: 'Server error'}),
handleApiError({
status: 500,
message: 'Server error',
responseData: null,
}),
).toThrow(Api.Errors.ServerSide)
})

it('should throw ServerSideError for other 5xx status codes', () => {
expect(() =>
handleApiError({status: 503, message: 'Service unavailable'}),
handleApiError({
status: 503,
message: 'Service unavailable',
responseData: null,
}),
).toThrow(Api.Errors.ServerSide)
expect(() =>
handleApiError({status: 504, message: 'Gateway timeout'}),
handleApiError({
status: 504,
message: 'Gateway timeout',
responseData: null,
}),
).toThrow(Api.Errors.ServerSide)
})

it('should throw UnknownError for unhandled status codes', () => {
expect(() =>
handleApiError({status: 999, message: 'Unknown error'}),
handleApiError({
status: 999,
message: 'Unknown error',
responseData: null,
}),
).toThrow(Api.Errors.Unknown)
})
})
2 changes: 2 additions & 0 deletions packages/resolver/src/adapters/handle/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe('getCryptoAddress', () => {
const errorApiResponse: Api.ResponseError = {
status: 404,
message: 'Not found',
responseData: null,
}

const mockFetchDataResponse: Left<Api.ResponseError> = {
Expand Down Expand Up @@ -127,6 +128,7 @@ describe('getCryptoAddress', () => {
const errorApiResponse: Api.ResponseError = {
status: 425,
message: 'Too Early',
responseData: null,
}

const mockFetchDataResponse: Left<Api.ResponseError> = {
Expand Down
37 changes: 37 additions & 0 deletions packages/resolver/src/adapters/unstoppable/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ describe('getCryptoAddress', () => {
const errorApiResponse: Api.ResponseError = {
status: 404,
message: 'Not found',
responseData: null,
}

const mockFetchDataResponse: Left<Api.ResponseError> = {
Expand Down Expand Up @@ -226,6 +227,7 @@ describe('getCryptoAddress', () => {
const errorApiResponse: Api.ResponseError = {
status: 425,
message: 'Too Early',
responseData: null,
}

const mockFetchDataResponse: Left<Api.ResponseError> = {
Expand All @@ -252,6 +254,41 @@ describe('getCryptoAddress', () => {
)
})

it('should throw an "UnsupportedTld" error when the api does not support a tld', async () => {
const domain = mockApiResponse.meta.domain
const expectedUrl = `${unstoppableApiConfig.mainnet.getCryptoAddress}${domain}`
const errorApiResponse: Api.ResponseError = {
status: 425,
message: 'Fake Message',
responseData: {
message: 'Unsupported TLD',
},
}

const mockFetchDataResponse: Left<Api.ResponseError> = {
tag: 'left',
error: errorApiResponse,
}
const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse)
const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions, {
request: mockFetchData,
})

await expect(() => getCryptoAddress(domain)).rejects.toThrow(
Resolver.Errors.UnsupportedTld,
)
expect(mockFetchData).toHaveBeenCalledWith(
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${mockOptions.apiKey}`,
},
url: expectedUrl,
},
undefined,
)
})

it('should build without dependencies (coverage only)', () => {
const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions)
expect(getCryptoAddress).toBeDefined()
Expand Down
36 changes: 27 additions & 9 deletions packages/resolver/src/adapters/unstoppable/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ export const unstoppableApiGetCryptoAddress = (
)

if (isLeft(response)) {
handleApiError(response.error)
const error = response.error as UnstoppableApiGetCryptoAddressError

if (error.responseData?.message?.includes('Unsupported TLD'))
throw new Resolver.Errors.UnsupportedTld()

handleApiError(error)
} else {
const parsedResponse = UnstoppableApiResponseSchema.parse(
response.value.data,
Expand Down Expand Up @@ -70,27 +75,40 @@ export type UnstoppableApiGetCryptoAddressResponse = {
}
}

export type UnstoppableApiGetCryptoAddressError = {
responseData: {
message: string
}
status: number
message: string
}

const UnstoppableApiResponseSchema = z.object({
records: z.object({
'crypto.ADA.address': z.string().optional(),
}),
})

// https://docs.unstoppabledomains.com/openapi/resolution/
export const unstoppableSupportedTlds = [
'.x',
'.crypto',
'.nft',
'.wallet',
'.polygon',
'.nft',
'.crypto',
'.blockchain',
'.bitcoin',
'.dao',
'.888',
'.zil',
'.wallet',
'.binanceus',
'.hi',
'.klever',
'.kresus',
'.anime',
'.manga',
'.go',
'.blockchain',
'.bitcoin',
'.zil',
'.eth',
'.com',
'.unstoppable',
] as const
export const isUnstoppableDomain = (value: string) => {
return unstoppableSupportedTlds.some((tld) => value.endsWith(tld))
Expand Down
26 changes: 16 additions & 10 deletions packages/resolver/src/utils/isResolvableDomain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@ import {isResolvableDomain} from './isResolvableDomain'
describe('isResolvableDomain', () => {
it.each`
resolve | expected
${'ud.com'} | ${true}
${'ud.eth'} | ${true}
${'ud.x'} | ${true}
${'ud.polygon'} | ${true}
${'ud.nft'} | ${true}
${'ud.crypto'} | ${true}
${'ud.zil'} | ${true}
${'ud.bitcoin'} | ${true}
${'ud.blockchain'} | ${true}
${'ud.go'} | ${true}
${'ud.888'} | ${true}
${'ud.bitcoin'} | ${true}
${'ud.dao'} | ${true}
${'ud.polygon'} | ${true}
${'ud.888'} | ${true}
${'ud.wallet'} | ${true}
${'ud.nft'} | ${true}
${'ud.x'} | ${true}
${'ud.unstoppable'} | ${true}
${'ud.binanceus'} | ${true}
${'ud.hi'} | ${true}
${'ud.klever'} | ${true}
${'ud.kresus'} | ${true}
${'ud.anime'} | ${true}
${'ud.manga'} | ${true}
${'ud.go'} | ${true}
${'ud.zil'} | ${true}
${'ud.eth'} | ${true}
${'$adahandle'} | ${true}
${'cns.ada'} | ${true}
${'ud.com'} | ${false}
${'ud.unstoppable'} | ${false}
${'other.uk'} | ${false}
${'$'} | ${false}
`('should return $expected for $resolve', ({resolve, expected}) => {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/api/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Either} from '../helpers/types'
export type ApiResponseError = {
status: number
message: string
responseData: unknown
}

export type ApiResponseSuccess<T> = {
Expand Down

0 comments on commit 79a1b60

Please sign in to comment.