Skip to content

Commit

Permalink
Implementation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-scheer committed Nov 25, 2021
1 parent 1b5b75a commit c705f2e
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 66 deletions.
23 changes: 21 additions & 2 deletions gateway-js/src/__tests__/execution-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,27 @@ export function getTestingSupergraphSdl(services: typeof fixtures = fixtures) {
);
}

export function wait(ms: number) {
return new Promise(r => setTimeout(r, ms));
export function wait(ms: number, toResolveTo?: any) {
return new Promise((r) => setTimeout(() => r(toResolveTo), ms));
}

export function waitUntil<T = void>() {
let userResolved: (value: T | PromiseLike<T>) => void;
let userRejected: (reason?: any) => void;
const promise = new Promise<T>(
(r) => ((userResolved = r), (userRejected = r)),
);
return [
promise,
// @ts-ignore
userResolved,
// @ts-ignore
userRejected,
] as [
Promise<T>,
(value: T | PromiseLike<T>) => void,
(reason?: any) => void,
];
}

export function printPlan(queryPlan: QueryPlan): string {
Expand Down
44 changes: 0 additions & 44 deletions gateway-js/src/__tests__/gateway/composedSdl.test.ts

This file was deleted.

1 change: 0 additions & 1 deletion gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ describe('lifecycle hooks', () => {
experimental_pollInterval: 10,
logger,
});
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith(
'Polling running services is dangerous and not recommended in production. Polling should only be used against a registry. If you are polling running services, use with caution.',
);
Expand Down
204 changes: 204 additions & 0 deletions gateway-js/src/__tests__/gateway/supergraphSdl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { ApolloGateway } from '@apollo/gateway';
import { fixturesWithUpdate } from 'apollo-federation-integration-testsuite';
import { createHash } from 'crypto';
import { ApolloServer } from 'apollo-server';
import { Logger } from 'apollo-server-types';
import { fetch } from '../../__mocks__/apollo-server-env';
import { getTestingSupergraphSdl, waitUntil } from '../execution-utils';

async function getSupergraphSdlGatewayServer() {
const server = new ApolloServer({
gateway: new ApolloGateway({
supergraphSdl: getTestingSupergraphSdl(),
}),
});

await server.listen({ port: 0 });
return server;
}

let logger: Logger;
beforeEach(() => {
logger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
});

describe('Using supergraphSdl static configuration', () => {
it('successfully starts and serves requests to the proper services', async () => {
const server = await getSupergraphSdlGatewayServer();

fetch.mockJSONResponseOnce({
data: { me: { username: '@jbaxleyiii' } },
});

const result = await server.executeOperation({
query: '{ me { username } }',
});

expect(result.data).toMatchInlineSnapshot(`
Object {
"me": Object {
"username": "@jbaxleyiii",
},
}
`);

const [url, request] = fetch.mock.calls[0];
expect(url).toEqual('https://accounts.api.com');
expect(request?.body).toEqual(
JSON.stringify({ query: '{me{username}}', variables: {} }),
);
await server.stop();
});
});

describe('Using supergraphSdl dynamic configuration', () => {
it('starts and remains in `initialized` state until user Promise resolves', async () => {
const [forGatewayToHaveCalledSupergraphSdl, resolve] = waitUntil();

const gateway = new ApolloGateway({
async supergraphSdl() {
resolve();
return new Promise(() => {});
},
});

await forGatewayToHaveCalledSupergraphSdl;
expect(gateway.__testing().state.phase).toEqual('initialized');
});

it('starts and waits in `initialized` state after calling load but before user Promise resolves', async () => {
const gateway = new ApolloGateway({
async supergraphSdl() {
return new Promise(() => {});
},
});

gateway.load();

expect(gateway.__testing().state.phase).toEqual('initialized');
});

it('moves from `initialized` to `loaded` state after calling `load()` and after user Promise resolves', async () => {
const [userPromise, resolveSupergraph] =
waitUntil<{ supergraphSdl: string }>();

const gateway = new ApolloGateway({
async supergraphSdl() {
return userPromise;
},
});

const loadPromise = gateway.load();
expect(gateway.__testing().state.phase).toEqual('initialized');

const supergraphSdl = getTestingSupergraphSdl();
const expectedCompositionId = createHash('sha256')
.update(supergraphSdl)
.digest('hex');
resolveSupergraph({ supergraphSdl });

await loadPromise;
const { state, compositionId } = gateway.__testing();
expect(state.phase).toEqual('loaded');
expect(compositionId).toEqual(expectedCompositionId);
});

it('updates its supergraph after user calls update function', async () => {
const [userPromise, resolveSupergraph] =
waitUntil<{ supergraphSdl: string }>();

let userUpdateFn: (updatedSupergraphSdl: string) => Promise<void>;
const gateway = new ApolloGateway({
async supergraphSdl(update) {
userUpdateFn = update;
return userPromise;
},
});

const supergraphSdl = getTestingSupergraphSdl();
const expectedId = createHash('sha256').update(supergraphSdl).digest('hex');
resolveSupergraph({ supergraphSdl: getTestingSupergraphSdl() });
await gateway.load();
expect(gateway.__testing().compositionId).toEqual(expectedId);

const updatedSupergraphSdl = getTestingSupergraphSdl(fixturesWithUpdate);
const expectedUpdatedId = createHash('sha256')
.update(updatedSupergraphSdl)
.digest('hex');
await userUpdateFn!(updatedSupergraphSdl);
expect(gateway.__testing().compositionId).toEqual(expectedUpdatedId);
});

it('calls user-provided `cleanup` function when stopped', async () => {
const cleanup = jest.fn(() => Promise.resolve());
const gateway = new ApolloGateway({
async supergraphSdl() {
return {
supergraphSdl: getTestingSupergraphSdl(),
cleanup,
};
},
});

await gateway.load();
const { state, compositionId } = gateway.__testing();
expect(state.phase).toEqual('loaded');
expect(compositionId).toEqual(
'562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b',
);

await gateway.stop();
expect(cleanup).toHaveBeenCalledTimes(1);
});

describe('errors', () => {
it('fails to load if user-provided `supergraphSdl` function throws', async () => {
const gateway = new ApolloGateway({
async supergraphSdl() {
throw new Error('supergraphSdl failed');
},
logger,
});

await expect(() =>
gateway.load(),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"User provided \`supergraphSdl\` function did not return an object containing a \`supergraphSdl\` property"`,
);

expect(gateway.__testing().state.phase).toEqual('failed to load');
expect(logger.error).toHaveBeenCalledWith(
'User-defined `supergraphSdl` function threw error: supergraphSdl failed',
);
});

it('gracefully handles Promise rejections from user `cleanup` function', async () => {
const rejectionMessage = 'thrown from cleanup function';
const cleanup = jest.fn(() =>
Promise.reject(rejectionMessage),
);
const gateway = new ApolloGateway({
async supergraphSdl() {
return {
supergraphSdl: getTestingSupergraphSdl(),
cleanup,
};
},
logger,
});

await gateway.load();
await expect(gateway.stop()).resolves.toBeUndefined();
expect(cleanup).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(
'Error occured while calling user provided `cleanup` function: ' +
rejectionMessage,
);
});
});
});
11 changes: 9 additions & 2 deletions gateway-js/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,17 @@ interface ExperimentalManuallyManagedSupergraphSdlGatewayConfig
experimental_updateSupergraphSdl: Experimental_UpdateSupergraphSdl;
}

export function isManuallyManagedSupergraphSdlGatewayConfig(
config: GatewayConfig,
): config is ManuallyManagedSupergraphSdlGatewayConfig {
return (
'supergraphSdl' in config && typeof config.supergraphSdl === 'function'
);
}
interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase {
supergraphSdl: (
update: (updatedSupergraphSdl: string) => void,
) => Promise<string | { supergraphSdl: string; cleanup: () => void }>;
update: (updatedSupergraphSdl: string) => Promise<void>,
) => Promise<{ supergraphSdl: string; cleanup?: () => Promise<void> }>;
}

type ManuallyManagedGatewayConfig =
Expand Down
Loading

0 comments on commit c705f2e

Please sign in to comment.