Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

Commit

Permalink
updated tests and added upgrade guides from links
Browse files Browse the repository at this point in the history
  • Loading branch information
James Baxley committed Oct 2, 2017
1 parent 50db4dc commit c66b1da
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 39 deletions.
131 changes: 128 additions & 3 deletions packages/apollo-link-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ An Apollo Link to allow sending a single http request per operation.

## Usage
```js
import HttpLink from "apollo-link-http";
import { HttpLink } from "apollo-link-http";

const link = new HttpLink({ uri: "/graphql" });

// or
import { createHttpLink } from "apollo-link-http";

const link = createHttpLink({ uri: "/graphql" });
```

## Options
Expand All @@ -21,16 +26,18 @@ HTTP Link takes an object with three options on it to customize the behavoir of
|---|---|---|---|
|uri|string|"/graphql"|false|
|includeExtensions|boolean|false|false|
|fetch|ApolloFetch|ApolloFetch|false|
|fetch|fetch|global fetch|false|

By default, this link uses the [Apollo Fetch](https://github.com/apollographql/apollo-fetch) library for the HTTP transport.

## Context
The HTTP Link uses the `headers` field on the context to allow passing headers to the HTTP request.
The HTTP Link uses the `headers` field on the context to allow passing headers to the HTTP request. It also supports the `credentials` field for defining credentials policy for fetch and `fetchOptions` to allow generic fetch overrides (i.e. method: "GET").

|name|value|default|required|
|---|---|---|---|
|headers|Headers (or object)|{}|false|
|credentials|string|none|false|
|fetchOptions|Fetch Options|none|false|

```js
import HttpLink from "apollo-link-http";
Expand All @@ -53,3 +60,121 @@ client.query({
}
})
```

### Upgrading from apollo-fetch / apollo-client
If you previously used either apollo-fetch or apollo-client, you will need to change the way `use` and `useAfter` are implemented in your app. They can both be implemented in a link like so:

#### Middleware

*Before*
```js
// before
import ApolloClient, { createNetworkInterface } from 'apollo-client';

const networkInterface = createNetworkInterface({ uri: '/graphql' });

networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // Create the header object if needed.
}
req.options.headers['authorization'] = localStorage.getItem('token') ? localStorage.getItem('token') : null;
next();
}
}]);

```

*After*
```js
import { ApolloLink } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';

const httpLink = createHttpLink({ uri: '/graphql' });
const middlewareLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
authorization: localStorage.getItem('token') || null
}
});
return forward(operation)
})

// use with apollo-client
const link = middewareLink.concat(httpLink);
```

#### Afterware (error)

*Before*
```js
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { logout } from './logout';

const networkInterface = createNetworkInterface({ uri: '/graphql' });

networkInterface.useAfter([{
applyAfterware({ response }, next) {
if (response.status === 401) {
logout();
}
next();
}
}]);
```
*After*

```js
import { ApolloLink } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';

import { logout } from './logout';

const httpLink = createHttpLink({ uri: '/graphql' });
const errorLink = onError(({ networkError }) => {
if (networkError.status === 401) {
logout();
}
})

// use with apollo-client
const link = middewareLink.concat(httpLink);
```

#### Afterware (data manipuliation)
*Before*
```js
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { logout } from './logout';

const networkInterface = createNetworkInterface({ uri: '/graphql' });

networkInterface.useAfter([{
applyAfterware({ response }, next) {
if (response.data.user.lastLoginDate) {
response.data.user.lastLoginDate = new Date(response.data.user.lastLoginDate)
}
next();
}
}]);
```

*After*
```js
import { ApolloLink } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';

const httpLink = createHttpLink({ uri: '/graphql' });
const addDatesLink = new ApolloLink((operation, forward) => {
return forward(operation).map((response) => {
if (response.data.user.lastLoginDate) {
response.data.user.lastLoginDate = new Date(response.data.user.lastLoginDate)
}
return response;
})
})

// use with apollo-client
const link = addDatesLink.concat(httpLink);
```
180 changes: 152 additions & 28 deletions packages/apollo-link-http/src/__tests__/httpLink.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ApolloLink, execute } from 'apollo-link';
import { createApolloFetch } from 'apollo-fetch';
import { Observable, ApolloLink, execute } from 'apollo-link';
import { print } from 'graphql';
import gql from 'graphql-tag';
import * as fetchMock from 'fetch-mock';

import { FetchLink as HttpLink, createFetchLink } from '../httpLink';
import { HttpLink, createHttpLink } from '../httpLink';

const sampleQuery = gql`
query SampleQuery {
Expand Down Expand Up @@ -214,27 +213,6 @@ describe('HttpLink', () => {
}, 50);
});

// xit('should add headers from the context', done => {
// const fetch = createApolloFetch({
// customFetch: (request, options) =>
// new Promise((resolve, reject) => {
// expect(options.headers['test']).toBeDefined();
// expect(options.headers.test).toEqual(context.headers.test);
// done();
// }),
// });
// const link = new HttpLink({ fetch });

// const context = {
// headers: {
// test: 'header',
// },
// };

// execute(link, { query: sampleQuery, context }).subscribe(() => {
// throw new Error();
// });
// });
it('adds headers to the request from the context', done => {
const variables = { params: 'stub' };
const middleware = new ApolloLink((operation, forward) => {
Expand All @@ -243,7 +221,7 @@ describe('HttpLink', () => {
});
return forward(operation);
});
const link = middleware.concat(createFetchLink({ uri: 'data' }));
const link = middleware.concat(createHttpLink({ uri: 'data' }));

execute(link, { query: sampleQuery, variables }).subscribe(result => {
const headers = fetchMock.lastCall()[1].headers;
Expand All @@ -255,7 +233,7 @@ describe('HttpLink', () => {
});
it('adds headers to the request from the context on an operation', done => {
const variables = { params: 'stub' };
const link = createFetchLink({ uri: 'data' });
const link = createHttpLink({ uri: 'data' });

const context = {
headers: { authorization: '1234' },
Expand All @@ -280,7 +258,7 @@ describe('HttpLink', () => {
});
return forward(operation);
});
const link = middleware.concat(createFetchLink({ uri: 'data' }));
const link = middleware.concat(createHttpLink({ uri: 'data' }));

execute(link, { query: sampleQuery, variables }).subscribe(result => {
const creds = fetchMock.lastCall()[1].credentials;
Expand All @@ -298,7 +276,7 @@ describe('HttpLink', () => {
});
return forward(operation);
});
const link = middleware.concat(createFetchLink({ uri: 'data' }));
const link = middleware.concat(createHttpLink({ uri: 'data' }));

execute(link, { query: sampleQuery, variables }).subscribe(result => {
const signal = fetchMock.lastCall()[1].signal;
Expand All @@ -307,3 +285,149 @@ describe('HttpLink', () => {
});
});
});

describe('dev warnings', () => {
it('warns if no fetch is present', done => {
if (typeof fetch !== 'undefined') fetch = undefined;
try {
const link = createHttpLink({ uri: 'data' });
done.fail("warning wasn't called");
} catch (e) {
expect(e.message).toMatch(/fetch is not found globally/);
done();
}
});
it('does not warn if no fetch is present but a fetch is passed', () => {
expect(() => {
const link = createHttpLink({ uri: 'data', fetch: () => {} });
}).not.toThrow();
});
it('warns if apollo-fetch is used', done => {
try {
const mockFetch = {
use: () => {},
useAfter: () => {},
batchUse: () => {},
batchUseAfter: () => {},
};
const link = createHttpLink({ uri: 'data', fetch: mockFetch });
done.fail("warning wasn't called");
} catch (e) {
expect(e.message).toMatch(/It looks like you're using apollo-fetch/);
done();
}
});
});

describe('error handling', () => {
const json = jest.fn(() => Promise.resolve({}));
const fetch = jest.fn((uri, options) => {
return Promise.resolve({ json });
});
it('throws an error if response code is > 300', done => {
fetch.mockReturnValueOnce(Promise.resolve({ status: 400, json }));
const link = createHttpLink({ uri: 'data', fetch });

execute(link, { query: sampleQuery }).subscribe(
result => {
done.fail('error should have been thrown from the network');
},
e => {
expect(e.parseError.message).toMatch(/Received status code 400/);
expect(e.statusCode).toBe(400);
done();
},
);
});
it('makes it easy to do stuff on a 401', done => {
fetch.mockReturnValueOnce(Promise.resolve({ status: 401, json }));

const middleware = new ApolloLink((operation, forward) => {
return new Observable(ob => {
const op = forward(operation);
const sub = op.subscribe({
next: ob.next.bind(ob),
error: e => {
expect(e.parseError.message).toMatch(/Received status code 401/);
expect(e.statusCode).toEqual(401);
ob.error(e);
done();
},
complete: ob.complete.bind(ob),
});

return () => {
sub.unsubscribe();
};
});
});

const link = middleware.concat(createHttpLink({ uri: 'data', fetch }));

execute(link, { query: sampleQuery }).subscribe(
result => {
done.fail('error should have been thrown from the network');
},
() => {},
);
});
it("throws if the body can't be stringified", done => {
fetch.mockReturnValueOnce(Promise.resolve({ data: {}, json }));
const link = createHttpLink({ uri: 'data', fetch });

let b;
const a = { b };
b = { a };
a.b = b;
const variables = {
a,
b,
};
execute(link, { query: sampleQuery, variables }).subscribe(
result => {
done.fail('error should have been thrown from the link');
},
e => {
expect(e.message).toMatch(/Payload is not serializable/);
expect(e.parseError.message).toMatch(
/Converting circular structure to JSON/,
);
done();
},
);
});
it('supports being cancelled and does not throw', done => {
let called;
class AbortController {
signal: {};
abort = () => {
called = true;
};
}

global.AbortController = AbortController;

fetch.mockReturnValueOnce(Promise.resolve({ json }));
const link = createHttpLink({ uri: 'data', fetch });

const sub = execute(link, { query: sampleQuery }).subscribe({
next: result => {
done.fail('result should not have been called');
},
error: e => {
done.fail('error should not have been called');
},
complete: () => {
done.fail('complete should not have been called');
},
});

sub.unsubscribe();

setTimeout(() => {
delete global.AbortController;
expect(called).toBe(true);
done();
}, 150);
});
});
Loading

0 comments on commit c66b1da

Please sign in to comment.