Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions src/__fixtures__/hooksTestHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ governing permissions and limitations under the License.
*/

import { buildHTTPExecutor, SyncFetchFn } from '@graphql-tools/executor-http';
import { createSchema, YogaInitialContext, YogaServer } from 'graphql-yoga';
import { createSchema, Plugin, YogaInitialContext, YogaServer } from 'graphql-yoga';
import { parse } from 'graphql';
import { HookFunctionPayload, HookResponse, HookStatus, UserContext } from '../types';
import { TypedExecutionArgs } from '@envelop/core';

const mockSuccessResponse: HookResponse = {
status: HookStatus.SUCCESS,
Expand All @@ -30,7 +31,15 @@ const convertMockResponseToContext = (mockResponse: HookResponse) =>
body: mockResponse,
}) as unknown as HookFunctionPayload;

const testFetch = (yogaServer: YogaServer<YogaInitialContext, UserContext>, query: string) => {
const mockSecrets = {
mockSecret: 'mockSecretValue',
};

const testFetch = (
yogaServer: YogaServer<YogaInitialContext, UserContext>,
query: string,
operationName?: string,
) => {
if (!('fetch' in yogaServer)) {
throw new Error('Unable to test YogaServer via fetch executor');
}
Expand All @@ -39,9 +48,11 @@ const testFetch = (yogaServer: YogaServer<YogaInitialContext, UserContext>, quer
fetch: yogaServer.fetch as SyncFetchFn,
});

const useOperationName = operationName || 'TestQuery';
return Promise.resolve(
executor({
document: parse(query),
operationName: useOperationName,
}),
);
};
Expand All @@ -60,14 +71,28 @@ const mockSchema = createSchema<UserContext>({
});

const mockQuery = /* GraphQL */ `
query {
query TestQuery {
hello
}
`;

async function extractArgsPlugin(
ref:
| TypedExecutionArgs<YogaInitialContext & UserContext>
| TypedExecutionArgs<YogaInitialContext>,
): Promise<Plugin<YogaInitialContext, UserContext>> {
return {
async onExecute({ args }) {
Object.assign(ref, args);
},
};
}

export {
convertMockResponseToContext,
extractArgsPlugin,
mockErrorResponse,
mockSecrets,
mockSchema,
mockSuccessResponse,
mockQuery,
Expand Down
67 changes: 56 additions & 11 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ OF ANY KIND, either express or implied. See the License for the specific languag
governing permissions and limitations under the License.
*/

import { TypedExecutionArgs } from '@envelop/core';
import { createYoga, YogaServer, YogaInitialContext } from 'graphql-yoga';
import { Readable } from 'node:stream';
import { beforeEach } from 'vitest';
import {
extractArgsPlugin,
mockErrorResponse,
mockQuery,
mockSchema,
mockSecrets,
mockSuccessResponse,
testFetch,
} from '../__fixtures__/hooksTestHelper';
Expand All @@ -27,6 +32,9 @@ let mockModule: Module;

describe('hooksPlugin', () => {
let yogaServer: YogaServer<YogaInitialContext, UserContext>;
const argsReference = {} as
| TypedExecutionArgs<YogaInitialContext & UserContext>
| TypedExecutionArgs<YogaInitialContext>;
beforeEach(async () => {
mockHook = vi.fn<HookFunction>();
mockModule = { mockHook };
Expand All @@ -41,7 +49,14 @@ describe('hooksPlugin', () => {
fn: 'mockHook',
},
}),
await extractArgsPlugin(argsReference),
],
context: initialContext => {
return {
...initialContext,
secrets: mockSecrets,
};
},
schema: mockSchema,
});
vi.resetAllMocks();
Expand All @@ -62,10 +77,29 @@ describe('hooksPlugin', () => {
});
test('should skip introspection queries', async () => {
expect(mockHook).toHaveBeenCalledTimes(0);
await testFetch(yogaServer, 'query IntrospectionQuery { __schema { types { name } } }');
await testFetch(
yogaServer,
'query IntrospectionQuery { __schema { types { name } } }',
'IntrospectionQuery',
);
expect(mockHook).toHaveBeenCalledTimes(0);
});
test('should update headers in context', async () => {
test('should have access to expected context/payload', async () => {
mockHook.mockImplementationOnce(() => mockSuccessResponse);
expect(mockHook).toHaveBeenCalledTimes(0);
await testFetch(yogaServer, mockQuery);
expect(mockHook).toHaveBeenCalledTimes(1);
expect(argsReference.contextValue.params).toBeDefined();
expect(argsReference.contextValue.request).toBeDefined();
expect(argsReference.contextValue.request.body).toBeInstanceOf(Readable);
const headers =
'headers' in argsReference.contextValue ? argsReference.contextValue.headers : undefined;
expect(headers).toEqual(undefined);
const secrets =
'secrets' in argsReference.contextValue ? argsReference.contextValue.secrets : undefined;
expect(secrets).toEqual(mockSecrets);
});
test('should be able to update headers in context', async () => {
mockHook.mockImplementation(() => {
return {
...mockSuccessResponse,
Expand All @@ -79,19 +113,30 @@ describe('hooksPlugin', () => {
expect(mockHook).toHaveBeenCalledTimes(0);
await testFetch(yogaServer, mockQuery);
expect(mockHook).toHaveBeenCalledTimes(1);
expect(argsReference.contextValue.params.query).toEqual(`query TestQuery{hello}`);
expect(argsReference.operationName).toEqual('TestQuery');
const headers =
'headers' in argsReference.contextValue ? argsReference.contextValue.headers : {};
expect(headers).toEqual(
expect.objectContaining({
'x-mock-header': 'mock-value',
}),
);
});
test('should invoke beforeAll hook', async () => {
expect(mockHook).toHaveBeenCalledTimes(0);
await testFetch(yogaServer, mockQuery);
expect(mockHook).toHaveBeenCalledTimes(1);
});
// test('should return GraphQL error when error', async () => {
// mockHook.mockImplementationOnce(() => mockErrorResponse);
// expect(mockHook).toHaveBeenCalledTimes(0);
// const response = await testFetch(yogaServer, mockQuery);
// const data = await response.json();
// expect(mockHook).toHaveBeenCalledTimes(1);
// expect(data.errors.length).toBe(1);
// expect(data.errors[0].message).toEqual(mockErrorResponse.message);
// });
test('should return GraphQL error when error', async () => {
mockHook.mockImplementationOnce(() => mockErrorResponse);
expect(mockHook).toHaveBeenCalledTimes(0);
const response = await testFetch(yogaServer, mockQuery);
expect(mockHook).toHaveBeenCalledTimes(1);
expect(response.data).toBeNull();
expect(response.errors).not.toBeUndefined();
const errors = response.errors!;
expect(errors.length).toBe(1);
expect(errors[0].message).toEqual(mockErrorResponse.message);
});
});
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export default async function hooksPlugin(config: PluginConfig): Promise<HooksPl
return {
async onExecute({ args, setResultAndStopExecution, extendContext }) {
const query = args.contextValue?.params?.query;
const operationName = args.operationName;
const { document, contextValue: context } = args;
const { params, request } = context || {};
const headers = Object.fromEntries(request.headers.entries());
Expand All @@ -63,6 +62,7 @@ export default async function hooksPlugin(config: PluginConfig): Promise<HooksPl
};

// Ignore introspection queries
const operationName = args.operationName;
const isIntrospectionQuery =
operationName === 'IntrospectionQuery' ||
(query && query.includes('query IntrospectionQuery'));
Expand Down
Loading