import Observable from 'zen-observable-ts'; import gql from 'graphql-tag'; import { print } from 'graphql/language/printer'; import { execute, ApolloLink, from, split, concat } from '../link'; import { MockLink, SetContextLink, testLinkResults } from '../test-utils'; import { FetchResult, Operation, NextLink } from '../types'; const sampleQuery = gql` query SampleQuery { stub { id } } `; const setContext = () => ({ add: 1 }); describe('ApolloLink(abstract class)', () => { describe('concat', () => { it('should concat a function', done => { const returnOne = new SetContextLink(setContext); const link = returnOne.concat((operation, forward) => { return Observable.of({ data: { count: operation.getContext().add } }); }); testLinkResults({ link, results: [{ count: 1 }], done, }); }); it('should concat a Link', done => { const returnOne = new SetContextLink(setContext); const mock = new MockLink(op => Observable.of({ data: op.getContext().add }), ); const link = returnOne.concat(mock); testLinkResults({ link, results: [1], done, }); }); it("should pass error to observable's error", done => { const error = new Error('thrown'); const returnOne = new SetContextLink(setContext); const mock = new MockLink( op => new Observable(observer => { observer.next({ data: op.getContext().add }); observer.error(error); }), ); const link = returnOne.concat(mock); testLinkResults({ link, results: [1, error], done, }); }); it('should concat a Link and function', done => { const returnOne = new SetContextLink(setContext); const mock = new MockLink((op, forward) => { op.setContext(({ add }) => ({ add: add + 2 })); return forward(op); }); const link = returnOne.concat(mock).concat(op => { return Observable.of({ data: op.getContext().add }); }); testLinkResults({ link, results: [3], done, }); }); it('should concat a function and Link', done => { const returnOne = new SetContextLink(setContext); const mock = new MockLink((op, forward) => Observable.of({ data: op.getContext().add }), ); const link = returnOne .concat((operation, forward) => { operation.setContext({ add: operation.getContext().add + 2, }); return forward(operation); }) .concat(mock); testLinkResults({ link, results: [3], done, }); }); it('should concat two functions', done => { const returnOne = new SetContextLink(setContext); const link = returnOne .concat((operation, forward) => { operation.setContext({ add: operation.getContext().add + 2, }); return forward(operation); }) .concat((op, forward) => Observable.of({ data: op.getContext().add })); testLinkResults({ link, results: [3], done, }); }); it('should concat two Links', done => { const returnOne = new SetContextLink(setContext); const mock1 = new MockLink((operation, forward) => { operation.setContext({ add: operation.getContext().add + 2, }); return forward(operation); }); const mock2 = new MockLink((op, forward) => Observable.of({ data: op.getContext().add }), ); const link = returnOne.concat(mock1).concat(mock2); testLinkResults({ link, results: [3], done, }); }); it("should return an link that can be concat'd multiple times", done => { const returnOne = new SetContextLink(setContext); const mock1 = new MockLink((operation, forward) => { operation.setContext({ add: operation.getContext().add + 2, }); return forward(operation); }); const mock2 = new MockLink((op, forward) => Observable.of({ data: op.getContext().add + 2 }), ); const mock3 = new MockLink((op, forward) => Observable.of({ data: op.getContext().add + 3 }), ); const link = returnOne.concat(mock1); testLinkResults({ link: link.concat(mock2), results: [5], }); testLinkResults({ link: link.concat(mock3), results: [6], done, }); }); }); describe('split', () => { it('should split two functions', done => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat((operation, forward) => Observable.of({ data: operation.getContext().add + 1 }), ); const link2 = returnOne.concat((operation, forward) => Observable.of({ data: operation.getContext().add + 2 }), ); const link = returnOne.split( operation => operation.getContext().add === 1, link1, link2, ); testLinkResults({ link, results: [2], }); context.add = 2; testLinkResults({ link, results: [4], done, }); }); it('should split two Links', done => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat( new MockLink((operation, forward) => Observable.of({ data: operation.getContext().add + 1 }), ), ); const link2 = returnOne.concat( new MockLink((operation, forward) => Observable.of({ data: operation.getContext().add + 2 }), ), ); const link = returnOne.split( operation => operation.getContext().add === 1, link1, link2, ); testLinkResults({ link, results: [2], }); context.add = 2; testLinkResults({ link, results: [4], done, }); }); it('should split a link and a function', done => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat((operation, forward) => Observable.of({ data: operation.getContext().add + 1 }), ); const link2 = returnOne.concat( new MockLink((operation, forward) => Observable.of({ data: operation.getContext().add + 2 }), ), ); const link = returnOne.split( operation => operation.getContext().add === 1, link1, link2, ); testLinkResults({ link, results: [2], }); context.add = 2; testLinkResults({ link, results: [4], done, }); }); it('should allow concat after split to be join', done => { const context = { test: true, add: 1 }; const start = new SetContextLink(() => ({ ...context })); const link = start .split( operation => operation.getContext().test, (operation, forward) => { operation.setContext(({ add }) => ({ add: add + 1 })); return forward(operation); }, (operation, forward) => { operation.setContext(({ add }) => ({ add: add + 2 })); return forward(operation); }, ) .concat(operation => Observable.of({ data: operation.getContext().add }), ); testLinkResults({ link, context, results: [2], }); context.test = false; testLinkResults({ link, context, results: [3], done, }); }); it('should allow default right to be empty or passthrough when forward available', done => { let context = { test: true }; const start = new SetContextLink(() => context); const link = start.split( operation => operation.getContext().test, operation => Observable.of({ data: { count: 1, }, }), ); const concat = link.concat(operation => Observable.of({ data: { count: 2, }, }), ); testLinkResults({ link, results: [{ count: 1 }], }); context.test = false; testLinkResults({ link, results: [], }); testLinkResults({ link: concat, results: [{ count: 2 }], done, }); }); }); describe('empty', () => { it('should returns an immediately completed Observable', done => { testLinkResults({ link: ApolloLink.empty(), done, }); }); }); }); describe('context', () => { it('should merge context when using a function', done => { const returnOne = new SetContextLink(setContext); const mock = new MockLink((op, forward) => { op.setContext(({ add }) => ({ add: add + 2 })); op.setContext(() => ({ substract: 1 })); return forward(op); }); const link = returnOne.concat(mock).concat(op => { expect(op.getContext()).toEqual({ add: 3, substract: 1, }); return Observable.of({ data: op.getContext().add }); }); testLinkResults({ link, results: [3], done, }); }); it('should merge context when not using a function', done => { const returnOne = new SetContextLink(setContext); const mock = new MockLink((op, forward) => { op.setContext({ add: 3 }); op.setContext({ substract: 1 }); return forward(op); }); const link = returnOne.concat(mock).concat(op => { expect(op.getContext()).toEqual({ add: 3, substract: 1, }); return Observable.of({ data: op.getContext().add }); }); testLinkResults({ link, results: [3], done, }); }); }); describe('Link static library', () => { describe('from', () => { const uniqueOperation: Operation = { query: sampleQuery, context: { name: 'uniqueName' }, operationName: 'SampleQuery', extensions: {}, }; it('should create an observable that completes when passed an empty array', done => { const observable = execute(from([]), { query: sampleQuery, }); observable.subscribe(() => expect(false), () => expect(false), done); }); it('can create chain of one', () => { expect(() => ApolloLink.from([new MockLink()])).not.toThrow(); }); it('can create chain of two', () => { expect(() => ApolloLink.from([ new MockLink((operation, forward) => forward(operation)), new MockLink(), ]), ).not.toThrow(); }); it('should receive result of one link', done => { const data: FetchResult = { data: { hello: 'world', }, }; const chain = ApolloLink.from([new MockLink(() => Observable.of(data))]); // Smoke tests execute as a static method const observable = ApolloLink.execute(chain, uniqueOperation); observable.subscribe({ next: actualData => { expect(data).toEqual(actualData); }, error: () => { throw new Error(); }, complete: () => done(), }); }); it('should accept AST query and pass AST to link', () => { const astOperation = { ...uniqueOperation, query: sampleQuery, }; const stub = jest.fn(); const chain = ApolloLink.from([new MockLink(stub)]); execute(chain, astOperation); expect(stub).toBeCalledWith({ query: sampleQuery, operationName: 'SampleQuery', variables: {}, extensions: {}, }); }); it('should pass operation from one link to next with modifications', done => { const chain = ApolloLink.from([ new MockLink((op, forward) => forward({ ...op, query: sampleQuery, }), ), new MockLink(op => { expect({ extensions: {}, operationName: 'SampleQuery', query: sampleQuery, variables: {}, }).toEqual(op); return done(); }), ]); execute(chain, uniqueOperation); }); it('should pass result of one link to another with forward', done => { const data: FetchResult = { data: { hello: 'world', }, }; const chain = ApolloLink.from([ new MockLink((op, forward) => { const observable = forward(op); observable.subscribe({ next: actualData => { expect(data).toEqual(actualData); }, error: () => { throw new Error(); }, complete: done, }); return observable; }), new MockLink(() => Observable.of(data)), ]); execute(chain, uniqueOperation); }); it('should receive final result of two link chain', done => { const data: FetchResult = { data: { hello: 'world', }, }; const chain = ApolloLink.from([ new MockLink((op, forward) => { const observable = forward(op); return new Observable(observer => { observable.subscribe({ next: actualData => { expect(data).toEqual(actualData); observer.next({ data: { ...actualData.data, modification: 'unique', }, }); }, error: error => observer.error(error), complete: () => observer.complete(), }); }); }), new MockLink(() => Observable.of(data)), ]); const result = execute(chain, uniqueOperation); result.subscribe({ next: modifiedData => { expect({ data: { ...data.data, modification: 'unique', }, }).toEqual(modifiedData); }, error: () => { throw new Error(); }, complete: done, }); }); it('should chain together a function with links', done => { const add1 = new ApolloLink((operation: Operation, forward: NextLink) => { operation.setContext(({ num }) => ({ num: num + 1 })); return forward(operation); }); const add1Link = new MockLink((operation, forward) => { operation.setContext(({ num }) => ({ num: num + 1 })); return forward(operation); }); const link = ApolloLink.from([ add1, add1, add1Link, add1, add1Link, new ApolloLink(operation => Observable.of({ data: operation.getContext() }), ), ]); testLinkResults({ link, results: [{ num: 5 }], context: { num: 0 }, done, }); }); }); describe('split', () => { it('should create filter when single link passed in', done => { const link = split( operation => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), ); let context = { test: true }; testLinkResults({ link, results: [{ count: 1 }], context, }); context.test = false; testLinkResults({ link, results: [], context, done, }); }); it('should split two functions', done => { const link = ApolloLink.split( operation => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), (operation, forward) => Observable.of({ data: { count: 2 } }), ); let context = { test: true }; testLinkResults({ link, results: [{ count: 1 }], context, }); context.test = false; testLinkResults({ link, results: [{ count: 2 }], context, done, }); }); it('should split two Links', done => { const link = ApolloLink.split( operation => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), new MockLink((operation, forward) => Observable.of({ data: { count: 2 } }), ), ); let context = { test: true }; testLinkResults({ link, results: [{ count: 1 }], context, }); context.test = false; testLinkResults({ link, results: [{ count: 2 }], context, done, }); }); it('should split a link and a function', done => { const link = ApolloLink.split( operation => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), new MockLink((operation, forward) => Observable.of({ data: { count: 2 } }), ), ); let context = { test: true }; testLinkResults({ link, results: [{ count: 1 }], context, }); context.test = false; testLinkResults({ link, results: [{ count: 2 }], context, done, }); }); it('should allow concat after split to be join', done => { const context = { test: true }; const link = ApolloLink.split( operation => operation.getContext().test, (operation, forward) => forward(operation).map(data => ({ data: { count: data.data.count + 1 }, })), ).concat(() => Observable.of({ data: { count: 1 } })); testLinkResults({ link, context, results: [{ count: 2 }], }); context.test = false; testLinkResults({ link, context, results: [{ count: 1 }], done, }); }); it('should allow default right to be passthrough', done => { const context = { test: true }; const link = ApolloLink.split( operation => operation.getContext().test, operation => Observable.of({ data: { count: 2 } }), ).concat(operation => Observable.of({ data: { count: 1 } })); testLinkResults({ link, context, results: [{ count: 2 }], }); context.test = false; testLinkResults({ link, context, results: [{ count: 1 }], done, }); }); }); describe('execute', () => { let _warn: (message?: any, ...originalParams: any[]) => void; beforeEach(() => { _warn = console.warn; console.warn = jest.fn(warning => { expect(warning).toBe(`query should either be a string or GraphQL AST`); }); }); afterEach(() => { console.warn = _warn; }); it('should return an empty observable when a link returns null', done => { testLinkResults({ link: new MockLink(), results: [], done, }); }); it('should return an empty observable when a link is empty', done => { testLinkResults({ link: ApolloLink.empty(), results: [], done, }); }); it("should return an empty observable when a concat'd link returns null", done => { const link = new MockLink((operation, forward) => { return forward(operation); }).concat(() => null); testLinkResults({ link, results: [], done, }); }); it('should return an empty observable when a split link returns null', done => { let context = { test: true }; const link = new SetContextLink(() => context).split( op => op.getContext().test, () => Observable.of(), () => null, ); testLinkResults({ link, results: [], }); context.test = false; testLinkResults({ link, results: [], done, }); }); it('should set a default context, variable, query and operationName on a copy of operation', done => { const operation = { query: gql` { id } `, }; const link = new ApolloLink(op => { expect(operation['operationName']).toBeUndefined(); expect(operation['variables']).toBeUndefined(); expect(operation['context']).toBeUndefined(); expect(operation['extensions']).toBeUndefined(); expect(op['operationName']).toBeDefined(); expect(op['variables']).toBeDefined(); expect(op['context']).toBeUndefined(); expect(op['extensions']).toBeDefined(); expect(op.toKey()).toBeDefined(); return Observable.of(); }); execute(link, operation).subscribe({ complete: done, }); }); }); }); describe('Terminating links', () => { const _warn = console.warn; const warningStub = jest.fn(warning => { expect(warning.message).toBe( `You are calling concat on a terminating link, which will have no effect`, ); }); const data = { stub: 'data', }; beforeAll(() => { console.warn = warningStub; }); beforeEach(() => { warningStub.mockClear(); }); afterAll(() => { console.warn = _warn; }); describe('concat', () => { it('should warn if attempting to concat to a terminating Link from function', () => { const link = new ApolloLink(operation => Observable.of({ data })); expect(concat(link, (operation, forward) => forward(operation))).toEqual( link, ); expect(warningStub).toHaveBeenCalledTimes(1); expect(warningStub.mock.calls[0][0].link).toEqual(link); }); it('should warn if attempting to concat to a terminating Link', () => { const link = new MockLink(operation => Observable.of()); expect(link.concat((operation, forward) => forward(operation))).toEqual( link, ); expect(warningStub).toHaveBeenCalledTimes(1); expect(warningStub.mock.calls[0][0].link).toEqual(link); }); it('should not warn if attempting concat a terminating Link at end', () => { const link = new MockLink((operation, forward) => forward(operation)); link.concat(operation => Observable.of()); expect(warningStub).not.toBeCalled(); }); }); describe('split', () => { it('should not warn if attempting to split a terminating and non-terminating Link', () => { const split = ApolloLink.split( () => true, operation => Observable.of({ data }), (operation, forward) => forward(operation), ); split.concat((operation, forward) => forward(operation)); expect(warningStub).not.toBeCalled(); }); it('should warn if attempting to concat to split two terminating links', () => { const split = ApolloLink.split( () => true, operation => Observable.of({ data }), operation => Observable.of({ data }), ); expect(split.concat((operation, forward) => forward(operation))).toEqual( split, ); expect(warningStub).toHaveBeenCalledTimes(1); }); it('should warn if attempting to split to split two terminating links', () => { const split = ApolloLink.split( () => true, operation => Observable.of({ data }), operation => Observable.of({ data }), ); expect( split.split( () => true, (operation, forward) => forward(operation), (operation, forward) => forward(operation), ), ).toEqual(split); expect(warningStub).toHaveBeenCalledTimes(1); }); }); describe('from', () => { it('should not warn if attempting to form a terminating then non-terminating Link', () => { ApolloLink.from([ (operation, forward) => forward(operation), operation => Observable.of({ data }), ]); expect(warningStub).not.toBeCalled(); }); it('should warn if attempting to add link after termination', () => { ApolloLink.from([ (operation, forward) => forward(operation), operation => Observable.of({ data }), (operation, forward) => forward(operation), ]); expect(warningStub).toHaveBeenCalledTimes(1); }); it('should warn if attempting to add link after termination', () => { ApolloLink.from([ new ApolloLink((operation, forward) => forward(operation)), new ApolloLink(operation => Observable.of({ data })), new ApolloLink((operation, forward) => forward(operation)), ]); expect(warningStub).toHaveBeenCalledTimes(1); }); }); describe('warning', () => { it('should include link that terminates', () => { const terminatingLink = new MockLink(operation => Observable.of({ data }), ); ApolloLink.from([ new ApolloLink((operation, forward) => forward(operation)), new ApolloLink((operation, forward) => forward(operation)), terminatingLink, new ApolloLink((operation, forward) => forward(operation)), new ApolloLink((operation, forward) => forward(operation)), new ApolloLink(operation => Observable.of({ data })), new ApolloLink((operation, forward) => forward(operation)), ]); expect(warningStub).toHaveBeenCalledTimes(4); }); }); }); describe('execute', () => { it('transforms an opearation with context into something serlizable', done => { const query = gql` { id } `; const link = new ApolloLink(operation => { const str = JSON.stringify({ ...operation, query: print(operation.query), }); expect(str).toBe( JSON.stringify({ variables: { id: 1 }, extensions: { cache: true }, operationName: null, query: print(operation.query), }), ); return Observable.of(); }); const noop = () => {}; execute(link, { query, variables: { id: 1 }, extensions: { cache: true }, }).subscribe(noop, noop, done); }); });