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

refactor: Replace "cache-control" extension with plugin. #3997

Merged
merged 9 commits into from May 12, 2020
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/apollo-cache-control/package.json
Expand Up @@ -12,7 +12,8 @@
},
"dependencies": {
"apollo-server-env": "file:../apollo-server-env",
"graphql-extensions": "file:../graphql-extensions"
"graphql-extensions": "file:../graphql-extensions",
"apollo-server-plugin-base": "file:../apollo-server-plugin-base"
},
"peerDependencies": {
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
Expand Down
@@ -1,59 +1,110 @@
import { ResponsePath, GraphQLError } from 'graphql';
import { GraphQLResponse } from 'graphql-extensions';
import { Headers } from 'apollo-server-env';
import { CacheControlExtension, CacheScope } from '../';
import {
CacheScope,
CacheControlExtensionOptions,
CacheHint,
__testing__,
plugin,
} from '../';
const { addHint, computeOverallCachePolicy } = __testing__;
import {
GraphQLRequestContextWillSendResponse,
} from 'apollo-server-plugin-base';
import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness';

describe('CacheControlExtension', () => {
let cacheControlExtension: CacheControlExtension;

beforeEach(() => {
cacheControlExtension = new CacheControlExtension();
});

describe('willSendResponse', () => {
let graphqlResponse: GraphQLResponse;
function makePluginWithOptions({
pluginInitializationOptions,
overallCachePolicy,
errors = false,
}: {
pluginInitializationOptions?: CacheControlExtensionOptions;
overallCachePolicy?: Required<CacheHint>;
errors?: boolean;
} = Object.create(null)) {
const pluginInstance = plugin(pluginInitializationOptions);

return pluginTestHarness({
pluginInstance,
overallCachePolicy,
graphqlRequest: { query: 'does not matter' },
executor: () => {
const response: GraphQLResponse = {
http: {
headers: new Headers(),
},
data: { test: 'test' },
};

if (errors) {
response.errors = [new GraphQLError('Test Error')];
}

return response;
},
});
}

beforeEach(() => {
cacheControlExtension.options.calculateHttpHeaders = true;
cacheControlExtension.computeOverallCachePolicy = () => ({
describe('HTTP cache-control header', () => {
const overallCachePolicy: Required<CacheHint> = {
maxAge: 300,
scope: CacheScope.Public,
});
graphqlResponse = {
http: {
headers: new Headers(),
},
data: { test: 'test' },
};
});

it('sets cache-control header', () => {
cacheControlExtension.willSendResponse &&
cacheControlExtension.willSendResponse({ graphqlResponse });
expect(graphqlResponse.http!.headers.get('Cache-Control')).toBe(
'max-age=300, public',
);
});
it('is set when calculateHttpHeaders is set to true', async () => {
const requestContext = await makePluginWithOptions({
pluginInitializationOptions: {
calculateHttpHeaders: true,
},
overallCachePolicy,
});
expect(requestContext.response.http!.headers.get('Cache-Control')).toBe(
'max-age=300, public',
);
});

const shouldNotSetCacheControlHeader = () => {
cacheControlExtension.willSendResponse &&
cacheControlExtension.willSendResponse({ graphqlResponse });
expect(graphqlResponse.http!.headers.get('Cache-Control')).toBeNull();
};
const shouldNotSetCacheControlHeader = (
requestContext: GraphQLRequestContextWillSendResponse<any>,
) => {
expect(
requestContext.response.http!.headers.get('Cache-Control'),
).toBeNull();
};

it('does not set cache-control header if calculateHttpHeaders is set to false', () => {
cacheControlExtension.options.calculateHttpHeaders = false;
shouldNotSetCacheControlHeader();
});
it('is not set when calculateHttpHeaders is set to false', async () => {
const requestContext = await makePluginWithOptions({
pluginInitializationOptions: {
calculateHttpHeaders: false,
},
overallCachePolicy,
});
shouldNotSetCacheControlHeader(requestContext);
});

it('does not set cache-control header if graphqlResponse has errors', () => {
graphqlResponse.errors = [new GraphQLError('Test Error')];
shouldNotSetCacheControlHeader();
});
it('is not set if response has errors', async () => {
const requestContext = await makePluginWithOptions({
pluginInitializationOptions: {
calculateHttpHeaders: false,
},
overallCachePolicy,
errors: true,
});
shouldNotSetCacheControlHeader(requestContext);
});

it('does not set cache-control header if there is no overall cache policy', () => {
cacheControlExtension.computeOverallCachePolicy = () => undefined;
shouldNotSetCacheControlHeader();
it('does not set cache-control header if there is no overall cache policy', async () => {
const requestContext = await makePluginWithOptions({
pluginInitializationOptions: {
calculateHttpHeaders: false,
},
overallCachePolicy: undefined,
errors: true,
});
shouldNotSetCacheControlHeader(requestContext);
});
});
});

Expand All @@ -71,46 +122,49 @@ describe('CacheControlExtension', () => {
prev: responseSubPath,
};

const hints = new Map();
afterEach(() => hints.clear());

it('returns undefined without cache hints', () => {
const cachePolicy = cacheControlExtension.computeOverallCachePolicy();
const cachePolicy = computeOverallCachePolicy(hints);
expect(cachePolicy).toBeUndefined();
});

it('returns lowest max age value', () => {
cacheControlExtension.addHint(responsePath, { maxAge: 10 });
cacheControlExtension.addHint(responseSubPath, { maxAge: 20 });
addHint(hints, responsePath, { maxAge: 10 });
addHint(hints, responseSubPath, { maxAge: 20 });

const cachePolicy = cacheControlExtension.computeOverallCachePolicy();
const cachePolicy = computeOverallCachePolicy(hints);
expect(cachePolicy).toHaveProperty('maxAge', 10);
});

it('returns undefined if any cache hint has a maxAge of 0', () => {
cacheControlExtension.addHint(responsePath, { maxAge: 120 });
cacheControlExtension.addHint(responseSubPath, { maxAge: 0 });
cacheControlExtension.addHint(responseSubSubPath, { maxAge: 20 });
addHint(hints, responsePath, { maxAge: 120 });
addHint(hints, responseSubPath, { maxAge: 0 });
addHint(hints, responseSubSubPath, { maxAge: 20 });

const cachePolicy = cacheControlExtension.computeOverallCachePolicy();
const cachePolicy = computeOverallCachePolicy(hints);
expect(cachePolicy).toBeUndefined();
});

it('returns PUBLIC scope by default', () => {
cacheControlExtension.addHint(responsePath, { maxAge: 10 });
addHint(hints, responsePath, { maxAge: 10 });

const cachePolicy = cacheControlExtension.computeOverallCachePolicy();
const cachePolicy = computeOverallCachePolicy(hints);
expect(cachePolicy).toHaveProperty('scope', CacheScope.Public);
});

it('returns PRIVATE scope if any cache hint has PRIVATE scope', () => {
cacheControlExtension.addHint(responsePath, {
addHint(hints, responsePath, {
maxAge: 10,
scope: CacheScope.Public,
});
cacheControlExtension.addHint(responseSubPath, {
addHint(hints, responseSubPath, {
maxAge: 10,
scope: CacheScope.Private,
});

const cachePolicy = cacheControlExtension.computeOverallCachePolicy();
const cachePolicy = computeOverallCachePolicy(hints);
expect(cachePolicy).toHaveProperty('scope', CacheScope.Private);
});
});
Expand Down
@@ -1,38 +1,41 @@
import { GraphQLSchema, graphql } from 'graphql';

import {
enableGraphQLExtensions,
GraphQLExtensionStack,
} from 'graphql-extensions';
import {
CacheControlExtension,
CacheHint,
CacheControlExtensionOptions,
plugin,
} from '../';
import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness';

export async function collectCacheControlHints(
schema: GraphQLSchema,
source: string,
options?: CacheControlExtensionOptions,
): Promise<CacheHint[]> {
enableGraphQLExtensions(schema);

// Because this test helper looks at the formatted extensions, we always want
// to include them.
const cacheControlExtension = new CacheControlExtension({
// to include them in the response rather than allow them to be stripped
// out.
const pluginInstance = plugin({
...options,
stripFormattedExtensions: false,
});

const response = await graphql({
const requestContext = await pluginTestHarness({
pluginInstance,
schema,
source,
contextValue: {
_extensionStack: new GraphQLExtensionStack([cacheControlExtension]),
graphqlRequest: {
query: source,
},
executor: async (requestContext) => {
return await graphql({
schema,
source: requestContext.request.query,
contextValue: requestContext.context,
});
}
});

expect(response.errors).toBeUndefined();
expect(requestContext.response.errors).toBeUndefined();

return cacheControlExtension.format()[1].hints;
return requestContext.response.extensions!.cacheControl.hints;
}