From 74e3d0908ffcfbc8712ae7b526af8e9927f2d6e9 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 15 Mar 2023 21:32:40 +0000 Subject: [PATCH] Add basic tests for persistedExchange --- .../persisted/src/persistedExchange.test.ts | 115 ++++++++++++++++++ exchanges/persisted/src/persistedExchange.ts | 58 ++++----- exchanges/persisted/src/sha256.ts | 2 +- 3 files changed, 146 insertions(+), 29 deletions(-) create mode 100644 exchanges/persisted/src/persistedExchange.test.ts diff --git a/exchanges/persisted/src/persistedExchange.test.ts b/exchanges/persisted/src/persistedExchange.test.ts new file mode 100644 index 0000000000..437ec3d2c5 --- /dev/null +++ b/exchanges/persisted/src/persistedExchange.test.ts @@ -0,0 +1,115 @@ +import { + Source, + pipe, + fromValue, + fromArray, + toPromise, + delay, + take, + tap, + map, +} from 'wonka'; + +import { Client, Operation, OperationResult, CombinedError } from '@urql/core'; + +import { vi, expect, it } from 'vitest'; +import { queryOperation } from '../../../packages/core/src/test-utils'; +import { persistedExchange } from './persistedExchange'; + +const makeExchangeArgs = () => { + const operations: Operation[] = []; + + const result = vi.fn( + (operation: Operation): OperationResult => ({ operation }) + ); + + return { + operations, + result, + exchangeArgs: { + forward: (op$: Source) => + pipe( + op$, + tap(op => operations.push(op)), + map(result) + ), + client: new Client({ url: '/api' }), + } as any, + }; +}; + +it('adds the APQ extensions correctly', async () => { + const { exchangeArgs } = makeExchangeArgs(); + + const res = await pipe( + fromValue(queryOperation), + persistedExchange()(exchangeArgs), + take(1), + toPromise + ); + + expect(res.operation.context.persistAttempt).toBe(true); + expect(res.operation.extensions).toEqual({ + persistedQuery: { + version: 1, + sha256Hash: expect.any(String), + miss: undefined, + }, + }); +}); + +it('retries query when persisted query resulted in miss', async () => { + const { result, operations, exchangeArgs } = makeExchangeArgs(); + + result.mockImplementationOnce(operation => ({ + operation, + error: new CombinedError({ + graphQLErrors: [{ message: 'PersistedQueryNotFound' }], + }), + })); + + const res = await pipe( + fromValue(queryOperation), + persistedExchange()(exchangeArgs), + take(1), + toPromise + ); + + expect(res.operation.context.persistAttempt).toBe(true); + expect(operations.length).toBe(2); + + expect(operations[1].extensions).toEqual({ + persistedQuery: { + version: 1, + sha256Hash: expect.any(String), + miss: true, + }, + }); +}); + +it('retries query persisted query resulted in unsupported', async () => { + const { result, operations, exchangeArgs } = makeExchangeArgs(); + + result.mockImplementationOnce(operation => ({ + operation, + error: new CombinedError({ + graphQLErrors: [{ message: 'PersistedQueryNotSupported' }], + }), + })); + + await pipe( + fromArray([queryOperation, queryOperation]), + delay(0), + persistedExchange()(exchangeArgs), + take(2), + toPromise + ); + + expect(operations.length).toBe(3); + + expect(operations[1].extensions).toEqual({ + persistedQuery: undefined, + }); + + expect(operations[2].extensions).toEqual(undefined); +}); diff --git a/exchanges/persisted/src/persistedExchange.ts b/exchanges/persisted/src/persistedExchange.ts index dd208996da..f632b02e0b 100644 --- a/exchanges/persisted/src/persistedExchange.ts +++ b/exchanges/persisted/src/persistedExchange.ts @@ -41,8 +41,6 @@ export const persistedExchange = ( ): Exchange => ({ forward }) => { if (!options) options = {}; - const retries = makeSubject(); - const preferGetForPersistedQueries = !!options.preferGetForPersistedQueries; const enforcePersistedQueries = !!options.enforcePersistedQueries; const hashFn = options.generateHash || hash; @@ -56,6 +54,7 @@ export const persistedExchange = ( operation.kind === 'query'); return operations$ => { + const retries = makeSubject(); const sharedOps$ = share(operations$); const forwardedOps$ = pipe( @@ -66,43 +65,46 @@ export const persistedExchange = ( const persistedOps$ = pipe( sharedOps$, filter(operationFilter), - mergeMap(operation => { + map(async operation => { const persistedOperation = makeOperation(operation.kind, operation, { ...operation.context, persistAttempt: true, }); - return pipe( - fromPromise( - hashFn(stringifyDocument(operation.query), operation.query) - ), - map(sha256Hash => { - if (sha256Hash) { - persistedOperation.extensions = { - ...persistedOperation.extensions, - persistedQuery: { - version: 1, - sha256Hash, - }, - }; - if ( - persistedOperation.kind === 'query' && - preferGetForPersistedQueries - ) { - persistedOperation.context.preferGetMethod = 'force'; - } - } - return persistedOperation; - }) + const sha256Hash = await hashFn( + stringifyDocument(operation.query), + operation.query ); - }) + if (sha256Hash) { + persistedOperation.extensions = { + ...persistedOperation.extensions, + persistedQuery: { + version: 1, + sha256Hash, + }, + }; + if ( + persistedOperation.kind === 'query' && + preferGetForPersistedQueries + ) { + persistedOperation.context.preferGetMethod = 'force'; + } + } + + return persistedOperation; + }), + mergeMap(fromPromise) ); return pipe( - merge([forwardedOps$, persistedOps$, retries.source]), + merge([persistedOps$, forwardedOps$, retries.source]), forward, map(result => { - if (!enforcePersistedQueries) { + if ( + !enforcePersistedQueries && + result.operation.extensions && + result.operation.extensions.persistedQuery + ) { if (result.error && isPersistedUnsupported(result.error)) { // Disable future persisted queries if they're not enforced supportsPersistedQueries = false; diff --git a/exchanges/persisted/src/sha256.ts b/exchanges/persisted/src/sha256.ts index 050ce56917..34ee7093be 100644 --- a/exchanges/persisted/src/sha256.ts +++ b/exchanges/persisted/src/sha256.ts @@ -22,7 +22,7 @@ const getNodeCrypto = async (): Promise => { }; export const hash = async (query: string): Promise => { - if (webCrypto) { + if (webCrypto && webCrypto.subtle) { const digest = await webCrypto.subtle.digest( { name: 'SHA-256' }, new TextEncoder().encode(query)