Skip to content

Commit eec87a6

Browse files
authored
Introduce an internal plugin test harness to facilitate plugin… (#3990)
* Introduce a plugin test harness to facilitate testing of plugins. This test harness is meant to avoid the need to do the more heavy execution which the request pipeline itself does within `processGraphQLRequest`. I'm not prepared to make this a public-facing harness just yet, but I have reason to believe that it could be beneficial for external plugin authors to take advantage of something like this - possibly within the context of `apollo-server-plugin-base`. There's perhaps a best-of-both-worlds approach here where the request pipeline could be tested against a more precise plugin API contract, but I'm deferring that work for now.
2 parents ab35c45 + dae59a5 commit eec87a6

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
WithRequired,
3+
GraphQLRequest,
4+
GraphQLRequestContextExecutionDidStart,
5+
GraphQLResponse,
6+
ValueOrPromise,
7+
GraphQLRequestContextWillSendResponse,
8+
GraphQLRequestContext,
9+
Logger,
10+
} from 'apollo-server-types';
11+
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql/type';
12+
import { CacheHint } from 'apollo-cache-control';
13+
import {
14+
enablePluginsForSchemaResolvers,
15+
symbolRequestListenerDispatcher,
16+
} from '../requestPipelineAPI';
17+
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
18+
import { InMemoryLRUCache } from 'apollo-server-caching';
19+
import { Dispatcher } from './dispatcher';
20+
21+
// This test harness guarantees the presence of `query`.
22+
type IPluginTestHarnessGraphqlRequest = WithRequired<GraphQLRequest, 'query'>;
23+
type IPluginTestHarnessExecutionDidStart<TContext> =
24+
GraphQLRequestContextExecutionDidStart<TContext> & {
25+
request: IPluginTestHarnessGraphqlRequest,
26+
};
27+
28+
export default async function pluginTestHarness<TContext>({
29+
pluginInstance,
30+
schema,
31+
logger,
32+
graphqlRequest,
33+
overallCachePolicy,
34+
executor,
35+
context = Object.create(null)
36+
}: {
37+
/**
38+
* An instance of the plugin to test.
39+
*/
40+
pluginInstance: ApolloServerPlugin<TContext>,
41+
42+
/**
43+
* The optional schema that will be received by the executor. If not
44+
* specified, a simple default schema will be created. In either case,
45+
* the schema will be mutated by wrapping the resolvers with the
46+
* `willResolveField` instrumentation that will allow it to respond to
47+
* that lifecycle hook's implementations plugins.
48+
*/
49+
schema?: GraphQLSchema;
50+
51+
/**
52+
* An optional logger (Defaults to `console`)
53+
*/
54+
logger?: Logger;
55+
56+
/**
57+
* The `GraphQLRequest` which will be received by the `executor`. The
58+
* `query` is required, and this doesn't support anything more exotic,
59+
* like automated persisted queries (APQ).
60+
*/
61+
graphqlRequest: IPluginTestHarnessGraphqlRequest;
62+
63+
/**
64+
* Overall cache control policy.
65+
*/
66+
overallCachePolicy?: Required<CacheHint>;
67+
68+
/**
69+
* This method will be executed to retrieve the response.
70+
*/
71+
executor: (
72+
requestContext: IPluginTestHarnessExecutionDidStart<TContext>,
73+
) => ValueOrPromise<GraphQLResponse>;
74+
75+
/**
76+
* (optional) To provide a user context, if necessary.
77+
*/
78+
context?: TContext;
79+
}): Promise<GraphQLRequestContextWillSendResponse<TContext>> {
80+
81+
if (!schema) {
82+
schema = new GraphQLSchema({
83+
query: new GraphQLObjectType({
84+
name: 'RootQueryType',
85+
fields: {
86+
hello: {
87+
type: GraphQLString,
88+
resolve() {
89+
return 'hello world';
90+
}
91+
}
92+
}
93+
})
94+
});
95+
}
96+
97+
enablePluginsForSchemaResolvers(schema);
98+
99+
const requestContext: GraphQLRequestContext<TContext> = {
100+
logger: logger || console,
101+
request: graphqlRequest,
102+
metrics: Object.create(null),
103+
source: graphqlRequest.query,
104+
cache: new InMemoryLRUCache(),
105+
context,
106+
};
107+
108+
requestContext.overallCachePolicy = overallCachePolicy;
109+
110+
if (typeof pluginInstance.requestDidStart !== "function") {
111+
throw new Error("Should be impossible as the plugin is defined.");
112+
}
113+
114+
const listener = pluginInstance.requestDidStart(requestContext);
115+
116+
if (!listener) {
117+
throw new Error("Should be impossible to not have a listener.");
118+
}
119+
120+
if (typeof listener.willResolveField !== 'function') {
121+
throw new Error("Should be impossible to not have 'willResolveField'.");
122+
}
123+
124+
const dispatcher = new Dispatcher([listener]);
125+
126+
// Put the dispatcher on the context so `willResolveField` can access it.
127+
Object.defineProperty(requestContext.context, symbolRequestListenerDispatcher, {
128+
value: dispatcher,
129+
});
130+
131+
const executionDidEnd = dispatcher.invokeDidStartHook(
132+
"executionDidStart",
133+
requestContext as IPluginTestHarnessExecutionDidStart<TContext>,
134+
);
135+
136+
try {
137+
// `response` is readonly, so we'll cast to `any` to assign to it.
138+
(requestContext.response as any) = await executor(
139+
requestContext as IPluginTestHarnessExecutionDidStart<TContext>,
140+
);
141+
executionDidEnd();
142+
} catch (executionError) {
143+
executionDidEnd(executionError);
144+
}
145+
146+
await dispatcher.invokeHookAsync(
147+
"willSendResponse",
148+
requestContext as GraphQLRequestContextWillSendResponse<TContext>,
149+
);
150+
151+
return requestContext as GraphQLRequestContextWillSendResponse<TContext>;
152+
}

0 commit comments

Comments
 (0)