Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure behaviour for missing request handlers #55

Merged
merged 4 commits into from
Apr 10, 2024
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
.DS_Store
.idea
.DS_Store
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,21 @@ const cache = new InMemoryCache({
const mockClient = createMockClient({ cache });
```

Additionally, you can specify a `missingHandlerPolicy` to define the behavior of the mock client when a request handler for a particular operation is not found.

The `missingHandlerPolicy` accepts one of three string values:
- `'throw-error'`: The client throws an error when it encounters a missing handler.
- `'warn-and-return-error'`: The client logs a warning message in the console and returns an error.
- `'return-error'`: The client returns an error without any warning message.

Here's an example of how you can set the `missingHandlerPolicy`:

```typescript
const mockClient = createMockClient({ missingHandlerPolicy: 'warn-and-return-error' });
```

In this example, if a request handler for a given operation is not found, the client will log a warning message to the console and then return an error.

Note: it is not possible to specify the `link` to use as this is how `mock-apollo-client` injects its behaviour.

### Fragments
Expand Down
8 changes: 6 additions & 2 deletions src/mockClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApolloClientOptions, ApolloClient, DocumentNode } from '@apollo/client/core';
import { InMemoryCache as Cache, NormalizedCacheObject } from '@apollo/client/cache';
import { MockLink } from './mockLink';
import { MissingHandlerPolicy, MockLink } from './mockLink';
import { IMockSubscription } from './mockSubscription';

export type RequestHandler<TData = any, TVariables = any> =
Expand All @@ -15,7 +15,11 @@ export type RequestHandlerResponse<T> =
export type MockApolloClient = ApolloClient<NormalizedCacheObject> &
{ setRequestHandler: (query: DocumentNode, handler: RequestHandler) => void };

export type MockApolloClientOptions = Partial<Omit<ApolloClientOptions<NormalizedCacheObject>, 'link'>> | undefined;
interface CustomOptions {
missingHandlerPolicy?: MissingHandlerPolicy;
}

export type MockApolloClientOptions = Partial<Omit<ApolloClientOptions<NormalizedCacheObject>, 'link'>> & CustomOptions | undefined;

export const createMockClient = (options?: MockApolloClientOptions): MockApolloClient => {
if ((options as any)?.link) {
Expand Down
63 changes: 50 additions & 13 deletions src/mockLink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ describe('class MockLink', () => {
const queryOne = gql`query One {one}`;
const queryTwo = gql`query Two {two}`;

const queryOneOperation = { query: queryOne, variables: { a: 'one' } } as Partial<Operation> as Operation;

const createMockObserver = (): jest.Mocked<Observer<any>> => ({
next: jest.fn(),
error: jest.fn(),
complete: jest.fn(),
});

beforeEach(() => {
jest.spyOn(console, 'warn')
.mockReset();
Expand Down Expand Up @@ -81,19 +89,6 @@ describe('class MockLink', () => {
});

describe('method request', () => {
const queryOneOperation = { query: queryOne, variables: { a: 'one' } } as Partial<Operation> as Operation;

const createMockObserver = (): jest.Mocked<Observer<any>> => ({
next: jest.fn(),
error: jest.fn(),
complete: jest.fn(),
});

it('throws when a handler is not defined for the query', () => {
expect(() => mockLink.request(queryOneOperation))
.toThrowError(`Request handler not defined for query: ${print(queryOne)}`)
});

it('correctly executes the handler when the handler is defined as a promise and it and successfully resolves', async () => {
const handler = jest.fn().mockResolvedValue({ data: 'Query one result' });
mockLink.setRequestHandler(queryOne, handler);
Expand Down Expand Up @@ -242,4 +237,46 @@ describe('class MockLink', () => {
expect(observer.complete).toBeCalledTimes(1);
});
});

describe('constructor option "missingHandlerPolicy"', () => {
it('when "throw-error" throws when a handler is not defined for the query', () => {
mockLink = new MockLink({missingHandlerPolicy: 'throw-error'})

expect(() => mockLink.request(queryOneOperation))
.toThrowError(`Request handler not defined for query: ${print(queryOne)}`)
});

it('when "warn-and-return-error" logs a warning when a handler is not defined for the query', async () => {
mockLink = new MockLink({missingHandlerPolicy: 'warn-and-return-error'})

const observable = mockLink.request(queryOneOperation);
const observer = createMockObserver();

observable.subscribe(observer);

await new Promise(r => setTimeout(r, 0));

expect(observer.next).not.toBeCalled();
expect(observer.error).toBeCalled();
expect(observer.complete).not.toBeCalled();
expect(console.warn).toBeCalledTimes(1);
expect(console.warn).toBeCalledWith(`Request handler not defined for query: ${print(queryOne)}`);
});

it('when "return-error" returns an error when a handler is not defined for the query', async () => {
mockLink = new MockLink({missingHandlerPolicy: 'return-error'})

const observable = mockLink.request(queryOneOperation);
const observer = createMockObserver();

observable.subscribe(observer);

await new Promise(r => setTimeout(r, 0));

expect(observer.next).not.toBeCalled();
expect(observer.error).toBeCalledTimes(1);
expect(observer.error).toBeCalledWith(new Error(`Request handler not defined for query: ${print(queryOne)}`));
expect(observer.complete).not.toBeCalled();
});
})
});
31 changes: 29 additions & 2 deletions src/mockLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,22 @@ import { RequestHandler, RequestHandlerResponse } from './mockClient';
import { removeClientSetsFromDocument, removeConnectionDirectiveFromDocument } from '@apollo/client/utilities';
import { IMockSubscription, MockSubscription } from './mockSubscription';

export type MissingHandlerPolicy = 'throw-error' | 'warn-and-return-error' | 'return-error';

interface MockLinkOptions {
missingHandlerPolicy?: MissingHandlerPolicy;
}

const DEFAULT_MISSING_HANDLER_POLICY: MissingHandlerPolicy = 'throw-error';

export class MockLink extends ApolloLink {
constructor(options?: MockLinkOptions) {
super();

this.missingHandlerPolicy = options?.missingHandlerPolicy || DEFAULT_MISSING_HANDLER_POLICY;
}

private readonly missingHandlerPolicy: MissingHandlerPolicy;
private requestHandlers: Record<string, RequestHandler | undefined> = {};

setRequestHandler(requestQuery: DocumentNode, handler: RequestHandler): void {
Expand All @@ -29,11 +44,19 @@ export class MockLink extends ApolloLink {

const handler = this.requestHandlers[key];

if (!handler) {
throw new Error(`Request handler not defined for query: ${print(operation.query)}`);
if (!handler && this.missingHandlerPolicy === 'throw-error') {
throw new Error(getNotDefinedHandlerMessage(operation));
}

return new Observable<FetchResult>(observer => {
if (!handler) {
if (this.missingHandlerPolicy === 'warn-and-return-error') {
console.warn(getNotDefinedHandlerMessage(operation));
}
throw new Error(getNotDefinedHandlerMessage(operation));
}


let result:
| Promise<RequestHandlerResponse<any>>
| IMockSubscription<any>
Expand Down Expand Up @@ -101,3 +124,7 @@ const isPromise = (maybePromise: any): maybePromise is Promise<any> =>

const isSubscription = (maybeSubscription: any): maybeSubscription is MockSubscription<any> =>
maybeSubscription && maybeSubscription instanceof MockSubscription;

const getNotDefinedHandlerMessage = (operation: Operation) => {
return `Request handler not defined for query: ${print(operation.query)}`
}
Loading