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 = { 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 = { data : { hello : 'world' , } , } ; const chain = ApolloLink . from ( [ new MockLink ( ( ) => Observable . of ( data ) ) ] ) ; 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 = { 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 = { 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 , forward ) => { 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 ; 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 ) ; } ) ; } ) ;