@apollo/gateway 0.300.0-alpha.2 → 2.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +1 -1
  3. package/dist/__generated__/graphqlTypes.d.ts +130 -0
  4. package/dist/__generated__/graphqlTypes.d.ts.map +1 -0
  5. package/dist/__generated__/graphqlTypes.js +25 -0
  6. package/dist/__generated__/graphqlTypes.js.map +1 -0
  7. package/dist/config.d.ts +104 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +47 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/datasources/LocalGraphQLDataSource.d.ts +3 -3
  12. package/dist/datasources/LocalGraphQLDataSource.d.ts.map +1 -1
  13. package/dist/datasources/LocalGraphQLDataSource.js +5 -5
  14. package/dist/datasources/LocalGraphQLDataSource.js.map +1 -1
  15. package/dist/datasources/RemoteGraphQLDataSource.d.ts +6 -4
  16. package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
  17. package/dist/datasources/RemoteGraphQLDataSource.js +60 -17
  18. package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
  19. package/dist/datasources/index.d.ts +1 -1
  20. package/dist/datasources/index.d.ts.map +1 -1
  21. package/dist/datasources/index.js +1 -0
  22. package/dist/datasources/index.js.map +1 -1
  23. package/dist/datasources/parseCacheControlHeader.d.ts +2 -0
  24. package/dist/datasources/parseCacheControlHeader.d.ts.map +1 -0
  25. package/dist/datasources/parseCacheControlHeader.js +16 -0
  26. package/dist/datasources/parseCacheControlHeader.js.map +1 -0
  27. package/dist/datasources/types.d.ts +16 -1
  28. package/dist/datasources/types.d.ts.map +1 -1
  29. package/dist/datasources/types.js +7 -0
  30. package/dist/datasources/types.js.map +1 -1
  31. package/dist/executeQueryPlan.d.ts +2 -1
  32. package/dist/executeQueryPlan.d.ts.map +1 -1
  33. package/dist/executeQueryPlan.js +199 -112
  34. package/dist/executeQueryPlan.js.map +1 -1
  35. package/dist/index.d.ts +62 -80
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +543 -234
  38. package/dist/index.js.map +1 -1
  39. package/dist/loadServicesFromRemoteEndpoint.d.ts +9 -9
  40. package/dist/loadServicesFromRemoteEndpoint.d.ts.map +1 -1
  41. package/dist/loadServicesFromRemoteEndpoint.js +13 -8
  42. package/dist/loadServicesFromRemoteEndpoint.js.map +1 -1
  43. package/dist/loadSupergraphSdlFromStorage.d.ts +13 -0
  44. package/dist/loadSupergraphSdlFromStorage.d.ts.map +1 -0
  45. package/dist/loadSupergraphSdlFromStorage.js +101 -0
  46. package/dist/loadSupergraphSdlFromStorage.js.map +1 -0
  47. package/dist/operationContext.d.ts +17 -0
  48. package/dist/operationContext.d.ts.map +1 -0
  49. package/dist/operationContext.js +42 -0
  50. package/dist/operationContext.js.map +1 -0
  51. package/dist/outOfBandReporter.d.ts +15 -0
  52. package/dist/outOfBandReporter.d.ts.map +1 -0
  53. package/dist/outOfBandReporter.js +88 -0
  54. package/dist/outOfBandReporter.js.map +1 -0
  55. package/dist/utilities/array.d.ts +1 -2
  56. package/dist/utilities/array.d.ts.map +1 -1
  57. package/dist/utilities/array.js +7 -14
  58. package/dist/utilities/array.js.map +1 -1
  59. package/dist/utilities/assert.d.ts +2 -0
  60. package/dist/utilities/assert.d.ts.map +1 -0
  61. package/dist/utilities/assert.js +10 -0
  62. package/dist/utilities/assert.js.map +1 -0
  63. package/dist/utilities/cleanErrorOfInaccessibleNames.d.ts +3 -0
  64. package/dist/utilities/cleanErrorOfInaccessibleNames.d.ts.map +1 -0
  65. package/dist/utilities/cleanErrorOfInaccessibleNames.js +27 -0
  66. package/dist/utilities/cleanErrorOfInaccessibleNames.js.map +1 -0
  67. package/dist/utilities/deepMerge.js +2 -2
  68. package/dist/utilities/deepMerge.js.map +1 -1
  69. package/dist/utilities/graphql.d.ts +1 -4
  70. package/dist/utilities/graphql.d.ts.map +1 -1
  71. package/dist/utilities/graphql.js +3 -36
  72. package/dist/utilities/graphql.js.map +1 -1
  73. package/dist/utilities/opentelemetry.d.ts +10 -0
  74. package/dist/utilities/opentelemetry.d.ts.map +1 -0
  75. package/dist/utilities/opentelemetry.js +19 -0
  76. package/dist/utilities/opentelemetry.js.map +1 -0
  77. package/package.json +30 -21
  78. package/src/__generated__/graphqlTypes.ts +140 -0
  79. package/src/__mocks__/apollo-server-env.ts +56 -0
  80. package/src/__mocks__/make-fetch-happen-fetcher.ts +55 -0
  81. package/src/__mocks__/tsconfig.json +7 -0
  82. package/src/__tests__/build-query-plan.feature +40 -311
  83. package/src/__tests__/buildQueryPlan.test.ts +246 -426
  84. package/src/__tests__/executeQueryPlan.test.ts +1691 -194
  85. package/src/__tests__/execution-utils.ts +33 -26
  86. package/src/__tests__/gateway/__snapshots__/opentelemetry.test.ts.snap +195 -0
  87. package/src/__tests__/gateway/buildService.test.ts +16 -19
  88. package/src/__tests__/gateway/composedSdl.test.ts +44 -0
  89. package/src/__tests__/gateway/endToEnd.test.ts +166 -0
  90. package/src/__tests__/gateway/executor.test.ts +49 -43
  91. package/src/__tests__/gateway/lifecycle-hooks.test.ts +58 -29
  92. package/src/__tests__/gateway/opentelemetry.test.ts +123 -0
  93. package/src/__tests__/gateway/queryPlanCache.test.ts +19 -20
  94. package/src/__tests__/gateway/reporting.test.ts +76 -55
  95. package/src/__tests__/integration/abstract-types.test.ts +1086 -22
  96. package/src/__tests__/integration/aliases.test.ts +5 -6
  97. package/src/__tests__/integration/boolean.test.ts +40 -38
  98. package/src/__tests__/integration/complex-key.test.ts +41 -56
  99. package/src/__tests__/integration/configuration.test.ts +321 -0
  100. package/src/__tests__/integration/custom-directives.test.ts +61 -46
  101. package/src/__tests__/integration/fragments.test.ts +8 -2
  102. package/src/__tests__/integration/list-key.test.ts +2 -2
  103. package/src/__tests__/integration/logger.test.ts +2 -2
  104. package/src/__tests__/integration/multiple-key.test.ts +11 -12
  105. package/src/__tests__/integration/mutations.test.ts +8 -5
  106. package/src/__tests__/integration/networkRequests.test.ts +447 -289
  107. package/src/__tests__/integration/nockMocks.ts +95 -66
  108. package/src/__tests__/integration/provides.test.ts +9 -6
  109. package/src/__tests__/integration/requires.test.ts +17 -15
  110. package/src/__tests__/integration/scope.test.ts +557 -0
  111. package/src/__tests__/integration/unions.test.ts +1 -1
  112. package/src/__tests__/integration/value-types.test.ts +35 -32
  113. package/src/__tests__/integration/variables.test.ts +8 -2
  114. package/src/__tests__/loadServicesFromRemoteEndpoint.test.ts +6 -2
  115. package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +694 -0
  116. package/src/__tests__/queryPlanCucumber.test.ts +11 -61
  117. package/src/__tests__/testSetup.ts +1 -4
  118. package/src/__tests__/tsconfig.json +2 -1
  119. package/src/config.ts +225 -0
  120. package/src/core/__tests__/core.test.ts +412 -0
  121. package/src/datasources/LocalGraphQLDataSource.ts +9 -10
  122. package/src/datasources/RemoteGraphQLDataSource.ts +117 -43
  123. package/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +11 -4
  124. package/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +148 -79
  125. package/src/datasources/__tests__/tsconfig.json +4 -2
  126. package/src/datasources/index.ts +1 -1
  127. package/src/datasources/parseCacheControlHeader.ts +43 -0
  128. package/src/datasources/types.ts +47 -2
  129. package/src/executeQueryPlan.ts +264 -153
  130. package/src/index.ts +925 -480
  131. package/src/loadServicesFromRemoteEndpoint.ts +24 -17
  132. package/src/loadSupergraphSdlFromStorage.ts +140 -0
  133. package/src/make-fetch-happen.d.ts +2 -2
  134. package/src/operationContext.ts +70 -0
  135. package/src/outOfBandReporter.ts +128 -0
  136. package/src/utilities/__tests__/cleanErrorOfInaccessibleElements.test.ts +104 -0
  137. package/src/utilities/__tests__/tsconfig.json +8 -0
  138. package/src/utilities/array.ts +6 -28
  139. package/src/utilities/assert.ts +14 -0
  140. package/src/utilities/cleanErrorOfInaccessibleNames.ts +29 -0
  141. package/src/utilities/graphql.ts +0 -64
  142. package/src/utilities/opentelemetry.ts +13 -0
  143. package/CHANGELOG.md +0 -226
  144. package/LICENSE.md +0 -20
  145. package/dist/FieldSet.d.ts +0 -18
  146. package/dist/FieldSet.d.ts.map +0 -1
  147. package/dist/FieldSet.js +0 -96
  148. package/dist/FieldSet.js.map +0 -1
  149. package/dist/QueryPlan.d.ts +0 -41
  150. package/dist/QueryPlan.d.ts.map +0 -1
  151. package/dist/QueryPlan.js +0 -15
  152. package/dist/QueryPlan.js.map +0 -1
  153. package/dist/buildQueryPlan.d.ts +0 -44
  154. package/dist/buildQueryPlan.d.ts.map +0 -1
  155. package/dist/buildQueryPlan.js +0 -670
  156. package/dist/buildQueryPlan.js.map +0 -1
  157. package/dist/loadServicesFromStorage.d.ts +0 -21
  158. package/dist/loadServicesFromStorage.d.ts.map +0 -1
  159. package/dist/loadServicesFromStorage.js +0 -64
  160. package/dist/loadServicesFromStorage.js.map +0 -1
  161. package/dist/snapshotSerializers/astSerializer.d.ts +0 -3
  162. package/dist/snapshotSerializers/astSerializer.d.ts.map +0 -1
  163. package/dist/snapshotSerializers/astSerializer.js +0 -14
  164. package/dist/snapshotSerializers/astSerializer.js.map +0 -1
  165. package/dist/snapshotSerializers/index.d.ts +0 -13
  166. package/dist/snapshotSerializers/index.d.ts.map +0 -1
  167. package/dist/snapshotSerializers/index.js +0 -15
  168. package/dist/snapshotSerializers/index.js.map +0 -1
  169. package/dist/snapshotSerializers/queryPlanSerializer.d.ts +0 -3
  170. package/dist/snapshotSerializers/queryPlanSerializer.d.ts.map +0 -1
  171. package/dist/snapshotSerializers/queryPlanSerializer.js +0 -78
  172. package/dist/snapshotSerializers/queryPlanSerializer.js.map +0 -1
  173. package/dist/snapshotSerializers/selectionSetSerializer.d.ts +0 -3
  174. package/dist/snapshotSerializers/selectionSetSerializer.d.ts.map +0 -1
  175. package/dist/snapshotSerializers/selectionSetSerializer.js +0 -12
  176. package/dist/snapshotSerializers/selectionSetSerializer.js.map +0 -1
  177. package/dist/snapshotSerializers/typeSerializer.d.ts +0 -3
  178. package/dist/snapshotSerializers/typeSerializer.d.ts.map +0 -1
  179. package/dist/snapshotSerializers/typeSerializer.js +0 -12
  180. package/dist/snapshotSerializers/typeSerializer.js.map +0 -1
  181. package/dist/utilities/MultiMap.d.ts +0 -4
  182. package/dist/utilities/MultiMap.d.ts.map +0 -1
  183. package/dist/utilities/MultiMap.js +0 -17
  184. package/dist/utilities/MultiMap.js.map +0 -1
  185. package/src/FieldSet.ts +0 -169
  186. package/src/QueryPlan.ts +0 -57
  187. package/src/__tests__/matchers/toCallService.ts +0 -105
  188. package/src/__tests__/matchers/toHaveBeenCalledBefore.ts +0 -40
  189. package/src/__tests__/matchers/toHaveFetched.ts +0 -81
  190. package/src/__tests__/matchers/toMatchAST.ts +0 -64
  191. package/src/buildQueryPlan.ts +0 -1190
  192. package/src/loadServicesFromStorage.ts +0 -170
  193. package/src/snapshotSerializers/astSerializer.ts +0 -21
  194. package/src/snapshotSerializers/index.ts +0 -21
  195. package/src/snapshotSerializers/queryPlanSerializer.ts +0 -144
  196. package/src/snapshotSerializers/selectionSetSerializer.ts +0 -13
  197. package/src/snapshotSerializers/typeSerializer.ts +0 -11
  198. package/src/utilities/MultiMap.ts +0 -11
@@ -1,43 +1,43 @@
1
1
  import nock from 'nock';
2
- import { fetch } from 'apollo-server-env';
2
+ import gql from 'graphql-tag';
3
+ import { DocumentNode, GraphQLObjectType, GraphQLSchema } from 'graphql';
4
+ import mockedEnv from 'mocked-env';
3
5
  import { Logger } from 'apollo-server-types';
4
- import { ApolloGateway, GCS_RETRY_COUNT, getDefaultGcsFetcher } from '../..';
6
+ import { ApolloGateway } from '../..';
5
7
  import {
6
- mockSDLQuerySuccess,
8
+ mockSdlQuerySuccess,
7
9
  mockServiceHealthCheckSuccess,
10
+ mockAllServicesHealthCheckSuccess,
8
11
  mockServiceHealthCheck,
9
- mockStorageSecretSuccess,
10
- mockStorageSecret,
11
- mockCompositionConfigLinkSuccess,
12
- mockCompositionConfigLink,
13
- mockCompositionConfigsSuccess,
14
- mockCompositionConfigs,
15
- mockImplementingServicesSuccess,
16
- mockImplementingServices,
17
- mockRawPartialSchemaSuccess,
18
- mockRawPartialSchema,
19
- apiKeyHash,
20
- graphId,
12
+ mockSupergraphSdlRequestSuccess,
13
+ mockSupergraphSdlRequest,
14
+ mockApolloConfig,
15
+ mockCloudConfigUrl,
16
+ mockSupergraphSdlRequestIfAfter,
17
+ mockSupergraphSdlRequestSuccessIfAfter,
21
18
  } from './nockMocks';
22
-
23
- import loadServicesFromStorage = require("../../loadServicesFromStorage");
24
-
25
- // This is a nice DX hack for GraphQL code highlighting and formatting within the file.
26
- // Anything wrapped within the gql tag within this file is just a string, not an AST.
27
- const gql = String.raw;
28
-
19
+ import {
20
+ accounts,
21
+ books,
22
+ documents,
23
+ fixturesWithUpdate,
24
+ inventory,
25
+ product,
26
+ reviews,
27
+ } from 'apollo-federation-integration-testsuite';
28
+ import { getTestingSupergraphSdl } from '../execution-utils';
29
+
30
+ type GenericFunction = (...args: unknown[]) => unknown;
29
31
  export interface MockService {
30
- gcsDefinitionPath: string;
31
- partialSchemaPath: string;
32
+ name: string;
32
33
  url: string;
33
- sdl: string;
34
+ typeDefs: DocumentNode;
34
35
  }
35
36
 
36
- const service: MockService = {
37
- gcsDefinitionPath: 'service-definition.json',
38
- partialSchemaPath: 'accounts-partial-schema.json',
37
+ const simpleService: MockService = {
38
+ name: 'accounts',
39
39
  url: 'http://localhost:4001',
40
- sdl: gql`
40
+ typeDefs: gql`
41
41
  extend type Query {
42
42
  me: User
43
43
  everyone: [User]
@@ -52,39 +52,19 @@ const service: MockService = {
52
52
  `,
53
53
  };
54
54
 
55
- const updatedService: MockService = {
56
- gcsDefinitionPath: 'updated-service-definition.json',
57
- partialSchemaPath: 'updated-accounts-partial-schema.json',
58
- url: 'http://localhost:4002',
59
- sdl: gql`
60
- extend type Query {
61
- me: User
62
- everyone: [User]
63
- }
64
-
65
- "This is my updated User"
66
- type User @key(fields: "id") {
67
- id: ID!
68
- name: String
69
- username: String
70
- }
71
- `,
72
- };
55
+ function getRootQueryFields(schema?: GraphQLSchema): string[] {
56
+ return Object.keys(
57
+ (schema?.getType('Query') as GraphQLObjectType).getFields(),
58
+ );
59
+ }
73
60
 
74
- let fetcher: typeof fetch;
75
61
  let logger: Logger;
62
+ let gateway: ApolloGateway | null = null;
63
+ let cleanUp: (() => void) | null = null;
76
64
 
77
65
  beforeEach(() => {
78
66
  if (!nock.isActive()) nock.activate();
79
67
 
80
- fetcher = getDefaultGcsFetcher().defaults({
81
- retry: {
82
- retries: GCS_RETRY_COUNT,
83
- minTimeout: 0,
84
- maxTimeout: 0,
85
- },
86
- });
87
-
88
68
  const warn = jest.fn();
89
69
  const debug = jest.fn();
90
70
  const error = jest.fn();
@@ -98,118 +78,265 @@ beforeEach(() => {
98
78
  };
99
79
  });
100
80
 
101
- afterEach(() => {
81
+ afterEach(async () => {
102
82
  expect(nock.isDone()).toBeTruthy();
103
83
  nock.cleanAll();
104
84
  nock.restore();
85
+ if (gateway) {
86
+ await gateway.stop();
87
+ gateway = null;
88
+ }
89
+
90
+ if (cleanUp) {
91
+ cleanUp();
92
+ cleanUp = null;
93
+ }
105
94
  });
106
95
 
107
96
  it('Queries remote endpoints for their SDLs', async () => {
108
- mockSDLQuerySuccess(service);
97
+ mockSdlQuerySuccess(simpleService);
109
98
 
110
- const gateway = new ApolloGateway({
111
- serviceList: [{ name: 'accounts', url: service.url }],
112
- logger
113
- });
99
+ gateway = new ApolloGateway({ serviceList: [simpleService] });
114
100
  await gateway.load();
115
101
  expect(gateway.schema!.getType('User')!.description).toBe('This is my User');
116
102
  });
117
103
 
118
- it('Extracts service definitions from remote storage', async () => {
119
- mockStorageSecretSuccess();
120
- mockCompositionConfigLinkSuccess();
121
- mockCompositionConfigsSuccess([service]);
122
- mockImplementingServicesSuccess(service);
123
- mockRawPartialSchemaSuccess(service);
104
+ it('Fetches Supergraph SDL from remote storage', async () => {
105
+ mockSupergraphSdlRequestSuccess();
124
106
 
125
- const gateway = new ApolloGateway({ logger });
107
+ gateway = new ApolloGateway({
108
+ logger,
109
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
110
+ });
126
111
 
127
- await gateway.load({ engine: { apiKeyHash, graphId } });
128
- expect(gateway.schema!.getType('User')!.description).toBe('This is my User');
112
+ await gateway.load(mockApolloConfig);
113
+ await gateway.stop();
114
+ expect(gateway.schema?.getType('User')).toBeTruthy();
129
115
  });
130
116
 
131
- it.each([
132
- ['warned', 'present'],
133
- ['not warned', 'absent'],
134
- ])('conflicting configurations are %s about when %s', async (_word, mode) => {
135
- const isConflict = mode === 'present';
136
- let blockerResolve: () => void;
137
- const blocker = new Promise(resolve => (blockerResolve = resolve));
138
- const original = loadServicesFromStorage.getServiceDefinitionsFromStorage;
139
- const spyGetServiceDefinitionsFromStorage = jest
140
- .spyOn(loadServicesFromStorage, 'getServiceDefinitionsFromStorage')
141
- .mockImplementationOnce(async (...args) => {
142
- try {
143
- return await original(...args);
144
- } catch (e) {
145
- throw e;
146
- } finally {
147
- setImmediate(blockerResolve);
148
- }
117
+ it('Fetches Supergraph SDL from remote storage using a configured env variable', async () => {
118
+ cleanUp = mockedEnv({
119
+ APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT: mockCloudConfigUrl,
120
+ });
121
+ mockSupergraphSdlRequestSuccess();
122
+
123
+ gateway = new ApolloGateway({
124
+ logger,
125
+ });
126
+
127
+ await gateway.load(mockApolloConfig);
128
+ await gateway.stop();
129
+ expect(gateway.schema?.getType('User')).toBeTruthy();
130
+ });
131
+
132
+ it('Updates Supergraph SDL from remote storage', async () => {
133
+ mockSupergraphSdlRequestSuccess();
134
+ mockSupergraphSdlRequestSuccessIfAfter(
135
+ 'originalId-1234',
136
+ 'updatedId-5678',
137
+ getTestingSupergraphSdl(fixturesWithUpdate),
138
+ );
139
+
140
+ // This test is only interested in the second time the gateway notifies of an
141
+ // update, since the first happens on load.
142
+ let secondUpdateResolve: GenericFunction;
143
+ const secondUpdate = new Promise((res) => (secondUpdateResolve = res));
144
+ const schemaChangeCallback = jest
145
+ .fn()
146
+ .mockImplementationOnce(() => undefined)
147
+ .mockImplementationOnce(() => {
148
+ secondUpdateResolve();
149
149
  });
150
150
 
151
- mockStorageSecretSuccess();
152
- if (isConflict) {
153
- mockCompositionConfigLinkSuccess();
154
- mockCompositionConfigsSuccess([service]);
155
- mockImplementingServicesSuccess(service);
156
- mockRawPartialSchemaSuccess(service);
157
- } else {
158
- mockCompositionConfigLink().reply(403);
159
- }
151
+ gateway = new ApolloGateway({
152
+ logger,
153
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
154
+ });
155
+ // eslint-disable-next-line
156
+ // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
157
+ gateway.experimental_pollInterval = 100;
158
+ gateway.onSchemaLoadOrUpdate(schemaChangeCallback);
159
+
160
+ await gateway.load(mockApolloConfig);
161
+ expect(gateway['compositionId']).toMatchInlineSnapshot(`"originalId-1234"`);
160
162
 
161
- mockSDLQuerySuccess(service);
163
+ await secondUpdate;
164
+ expect(gateway['compositionId']).toMatchInlineSnapshot(`"updatedId-5678"`);
165
+ });
166
+
167
+ describe('Supergraph SDL update failures', () => {
168
+ it('Gateway throws on initial load failure', async () => {
169
+ mockSupergraphSdlRequest().reply(401);
170
+
171
+ gateway = new ApolloGateway({
172
+ logger,
173
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
174
+ });
162
175
 
163
- const gateway = new ApolloGateway({
164
- serviceList: [
165
- { name: 'accounts', url: service.url },
166
- ],
167
- logger
176
+ await expect(
177
+ gateway.load(mockApolloConfig),
178
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
179
+ `"An error occurred while fetching your schema from Apollo: 401 Unauthorized"`,
180
+ );
181
+
182
+ await expect(gateway.stop()).rejects.toThrowErrorMatchingInlineSnapshot(
183
+ `"ApolloGateway.stop does not need to be called before ApolloGateway.load is called successfully"`,
184
+ );
185
+ // Set to `null` so we don't try to call `stop` on it in the `afterEach`,
186
+ // which triggers a different error that we're not testing for here.
187
+ gateway = null;
168
188
  });
169
189
 
170
- await gateway.load({ engine: { apiKeyHash, graphId } });
171
- await blocker; // Wait for the definitions to be "fetched".
190
+ it('Handles arbitrary fetch failures (non 200 response)', async () => {
191
+ mockSupergraphSdlRequestSuccess();
192
+ mockSupergraphSdlRequestIfAfter('originalId-1234').reply(500);
193
+
194
+ // Spy on logger.error so we can just await once it's been called
195
+ let errorLogged: GenericFunction;
196
+ const errorLoggedPromise = new Promise((r) => (errorLogged = r));
197
+ logger.error = jest.fn(() => errorLogged());
198
+
199
+ gateway = new ApolloGateway({
200
+ logger,
201
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
202
+ });
203
+
204
+ // eslint-disable-next-line
205
+ // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
206
+ gateway.experimental_pollInterval = 100;
207
+
208
+ await gateway.load(mockApolloConfig);
209
+ await errorLoggedPromise;
210
+
211
+ expect(logger.error).toHaveBeenCalledWith(
212
+ 'An error occurred while fetching your schema from Apollo: 500 Internal Server Error',
213
+ );
214
+ });
215
+
216
+ it('Handles GraphQL errors', async () => {
217
+ mockSupergraphSdlRequestSuccess();
218
+ mockSupergraphSdlRequest('originalId-1234').reply(200, {
219
+ errors: [
220
+ {
221
+ message: 'Cannot query field "fail" on type "Query".',
222
+ locations: [{ line: 1, column: 3 }],
223
+ extensions: { code: 'GRAPHQL_VALIDATION_FAILED' },
224
+ },
225
+ ],
226
+ });
227
+
228
+ // Spy on logger.error so we can just await once it's been called
229
+ let errorLogged: GenericFunction;
230
+ const errorLoggedPromise = new Promise((r) => (errorLogged = r));
231
+ logger.error = jest.fn(() => errorLogged());
232
+
233
+ gateway = new ApolloGateway({
234
+ logger,
235
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
236
+ });
237
+ // eslint-disable-next-line
238
+ // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
239
+ gateway.experimental_pollInterval = 100;
240
+
241
+ await gateway.load(mockApolloConfig);
242
+ await errorLoggedPromise;
243
+
244
+ expect(logger.error).toHaveBeenCalledWith(
245
+ 'An error occurred while fetching your schema from Apollo: ' +
246
+ '\n' +
247
+ 'Cannot query field "fail" on type "Query".',
248
+ );
249
+ });
250
+
251
+ it("Doesn't update and logs on receiving unparseable Supergraph SDL", async () => {
252
+ mockSupergraphSdlRequestSuccess();
253
+ mockSupergraphSdlRequestIfAfter('originalId-1234').reply(
254
+ 200,
255
+ JSON.stringify({
256
+ data: {
257
+ routerConfig: {
258
+ __typename: 'RouterConfigResult',
259
+ id: 'failure',
260
+ supergraphSdl: 'Syntax Error - invalid SDL',
261
+ },
262
+ },
263
+ }),
264
+ );
265
+
266
+ // Spy on logger.error so we can just await once it's been called
267
+ let errorLogged: GenericFunction;
268
+ const errorLoggedPromise = new Promise((r) => (errorLogged = r));
269
+ logger.error = jest.fn(() => errorLogged());
270
+
271
+ gateway = new ApolloGateway({
272
+ logger,
273
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
274
+ });
275
+ // eslint-disable-next-line
276
+ // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
277
+ gateway.experimental_pollInterval = 100;
172
278
 
173
- (isConflict
174
- ? expect(logger.warn)
175
- : expect(logger.warn).not
176
- ).toHaveBeenCalledWith(expect.stringMatching(
177
- /A local gateway service list is overriding an Apollo Graph Manager managed configuration/));
178
- spyGetServiceDefinitionsFromStorage.mockRestore();
279
+ await gateway.load(mockApolloConfig);
280
+ await errorLoggedPromise;
281
+
282
+ expect(logger.error).toHaveBeenCalledWith(
283
+ 'Syntax Error: Unexpected Name "Syntax".',
284
+ );
285
+ expect(gateway.schema).toBeTruthy();
286
+ });
287
+
288
+ it('Throws on initial load when receiving unparseable Supergraph SDL', async () => {
289
+ mockSupergraphSdlRequest().reply(
290
+ 200,
291
+ JSON.stringify({
292
+ data: {
293
+ routerConfig: {
294
+ __typename: 'RouterConfigResult',
295
+ id: 'failure',
296
+ supergraphSdl: 'Syntax Error - invalid SDL',
297
+ },
298
+ },
299
+ }),
300
+ );
301
+
302
+ gateway = new ApolloGateway({
303
+ logger,
304
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
305
+ });
306
+ // eslint-disable-next-line
307
+ // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
308
+ gateway.experimental_pollInterval = 100;
309
+
310
+ await expect(
311
+ gateway.load(mockApolloConfig),
312
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
313
+ `"Syntax Error: Unexpected Name \\"Syntax\\"."`,
314
+ );
315
+
316
+ expect(gateway['state'].phase).toEqual('failed to load');
317
+
318
+ // Set to `null` so we don't try to call `stop` on it in the `afterEach`,
319
+ // which triggers a different error that we're not testing for here.
320
+ gateway = null;
321
+ });
179
322
  });
180
323
 
181
- // This test has been flaky for a long time, and fails consistently after changes
182
- // introduced by https://github.com/apollographql/apollo-server/pull/4277.
183
- // I've decided to skip this test for now with hopes that we can one day
184
- // determine the root cause and test this behavior in a reliable manner.
185
- it.skip('Rollsback to a previous schema when triggered', async () => {
324
+ it('Rollsback to a previous schema when triggered', async () => {
186
325
  // Init
187
- mockStorageSecretSuccess();
188
- mockCompositionConfigLinkSuccess();
189
- mockCompositionConfigsSuccess([service]);
190
- mockImplementingServicesSuccess(service);
191
- mockRawPartialSchemaSuccess(service);
192
-
193
- // Update 1
194
- mockStorageSecretSuccess();
195
- mockCompositionConfigLinkSuccess();
196
- mockCompositionConfigsSuccess([updatedService]);
197
- mockImplementingServicesSuccess(updatedService);
198
- mockRawPartialSchemaSuccess(updatedService);
199
-
200
- // Rollback
201
- mockStorageSecretSuccess();
202
- mockCompositionConfigLinkSuccess();
203
- mockCompositionConfigsSuccess([service]);
204
- mockImplementingServices(service).reply(304);
205
- mockRawPartialSchema(service).reply(304);
206
-
207
- let firstResolve: () => void;
208
- let secondResolve: () => void;
209
- let thirdResolve: () => void
210
- const firstSchemaChangeBlocker = new Promise(res => (firstResolve = res));
211
- const secondSchemaChangeBlocker = new Promise(res => (secondResolve = res));
212
- const thirdSchemaChangeBlocker = new Promise(res => (thirdResolve = res));
326
+ mockSupergraphSdlRequestSuccess();
327
+ mockSupergraphSdlRequestSuccessIfAfter(
328
+ 'originalId-1234',
329
+ 'updatedId-5678',
330
+ getTestingSupergraphSdl(fixturesWithUpdate),
331
+ );
332
+ mockSupergraphSdlRequestSuccessIfAfter('updatedId-5678');
333
+
334
+ let firstResolve: GenericFunction;
335
+ let secondResolve: GenericFunction;
336
+ let thirdResolve: GenericFunction;
337
+ const firstSchemaChangeBlocker = new Promise((res) => (firstResolve = res));
338
+ const secondSchemaChangeBlocker = new Promise((res) => (secondResolve = res));
339
+ const thirdSchemaChangeBlocker = new Promise((res) => (thirdResolve = res));
213
340
 
214
341
  const onChange = jest
215
342
  .fn()
@@ -217,12 +344,16 @@ it.skip('Rollsback to a previous schema when triggered', async () => {
217
344
  .mockImplementationOnce(() => secondResolve())
218
345
  .mockImplementationOnce(() => thirdResolve());
219
346
 
220
- const gateway = new ApolloGateway({ logger });
347
+ gateway = new ApolloGateway({
348
+ logger,
349
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
350
+ });
351
+ // eslint-disable-next-line
221
352
  // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
222
353
  gateway.experimental_pollInterval = 100;
223
354
 
224
355
  gateway.onSchemaChange(onChange);
225
- await gateway.load({ engine: { apiKeyHash, graphId } });
356
+ await gateway.load(mockApolloConfig);
226
357
 
227
358
  await firstSchemaChangeBlocker;
228
359
  expect(onChange).toHaveBeenCalledTimes(1);
@@ -234,208 +365,217 @@ it.skip('Rollsback to a previous schema when triggered', async () => {
234
365
  expect(onChange).toHaveBeenCalledTimes(3);
235
366
  });
236
367
 
237
- function failNTimes(n: number, fn: () => nock.Interceptor) {
238
- for (let i = 0; i < n; i++) {
239
- fn().reply(500);
240
- }
241
- }
242
-
243
- it(`Retries GCS (up to ${GCS_RETRY_COUNT} times) on failure for each request and succeeds`, async () => {
244
- failNTimes(GCS_RETRY_COUNT, mockStorageSecret);
245
- mockStorageSecretSuccess();
246
-
247
- failNTimes(GCS_RETRY_COUNT, mockCompositionConfigLink);
248
- mockCompositionConfigLinkSuccess();
249
-
250
- failNTimes(GCS_RETRY_COUNT, mockCompositionConfigs);
251
- mockCompositionConfigsSuccess([service]);
252
-
253
- failNTimes(GCS_RETRY_COUNT, () => mockImplementingServices(service));
254
- mockImplementingServicesSuccess(service);
255
-
256
- failNTimes(GCS_RETRY_COUNT, () => mockRawPartialSchema(service));
257
- mockRawPartialSchemaSuccess(service);
258
-
259
- const gateway = new ApolloGateway({ fetcher, logger });
260
-
261
- await gateway.load({ engine: { apiKeyHash, graphId } });
262
- expect(gateway.schema!.getType('User')!.description).toBe('This is my User');
263
- });
264
-
265
- // This test is reliably failing in its current form. It's mostly testing that
266
- // `make-fetch-happen` is doing its retries properly and we have proof that,
267
- // generally speaking, retries are working, so we'll disable this until we can
268
- // re-visit it.
269
- it.skip(`Fails after the ${GCS_RETRY_COUNT + 1}th attempt to reach GCS`, async () => {
270
- failNTimes(GCS_RETRY_COUNT + 1, mockStorageSecret);
271
-
272
- const gateway = new ApolloGateway({ fetcher, logger });
273
- await expect(
274
- gateway.load({ engine: { apiKeyHash, graphId } }),
275
- ).rejects.toThrowErrorMatchingInlineSnapshot(
276
- `"Could not communicate with Apollo Graph Manager storage: "`,
277
- );
278
- });
279
-
280
- it(`Errors when the secret isn't hosted on GCS`, async () => {
281
- mockStorageSecret().reply(
282
- 403,
283
- `<Error><Code>AccessDenied</Code>
284
- Anonymous caller does not have storage.objects.get`,
285
- { 'content-type': 'application/xml' },
286
- );
287
-
288
- const gateway = new ApolloGateway({ fetcher, logger });
289
- await expect(
290
- gateway.load({ engine: { apiKeyHash, graphId } }),
291
- ).rejects.toThrowErrorMatchingInlineSnapshot(
292
- `"Unable to authenticate with Apollo Graph Manager storage while fetching https://storage-secrets.api.apollographql.com/federated-service/storage-secret/dd55a79d467976346d229a7b12b673ce.json. Ensure that the API key is configured properly and that a federated service has been pushed. For details, see https://go.apollo.dev/g/resolve-access-denied."`,
293
- );
294
- });
295
-
296
368
  describe('Downstream service health checks', () => {
297
369
  describe('Unmanaged mode', () => {
298
370
  it(`Performs health checks to downstream services on load`, async () => {
299
- mockSDLQuerySuccess(service);
300
- mockServiceHealthCheckSuccess(service);
371
+ mockSdlQuerySuccess(simpleService);
372
+ mockServiceHealthCheckSuccess(simpleService);
301
373
 
302
- const gateway = new ApolloGateway({
374
+ gateway = new ApolloGateway({
303
375
  logger,
304
- serviceList: [{ name: 'accounts', url: service.url }],
376
+ serviceList: [simpleService],
305
377
  serviceHealthCheck: true,
306
378
  });
307
379
 
308
380
  await gateway.load();
309
- expect(gateway.schema!.getType('User')!.description).toBe('This is my User');
381
+ expect(gateway.schema!.getType('User')!.description).toBe(
382
+ 'This is my User',
383
+ );
310
384
  });
311
385
 
312
386
  it(`Rejects on initial load when health check fails`, async () => {
313
- mockSDLQuerySuccess(service);
314
- mockServiceHealthCheck(service).reply(500);
387
+ mockSdlQuerySuccess(simpleService);
388
+ mockServiceHealthCheck(simpleService).reply(500);
315
389
 
316
- const gateway = new ApolloGateway({
317
- serviceList: [{ name: 'accounts', url: service.url }],
390
+ gateway = new ApolloGateway({
391
+ serviceList: [simpleService],
318
392
  serviceHealthCheck: true,
319
393
  logger,
320
394
  });
321
395
 
322
- await expect(gateway.load()).rejects.toThrowErrorMatchingInlineSnapshot(
323
- `"500: Internal Server Error"`,
396
+ // This is the ideal, but our version of Jest has a bug with printing error snapshots.
397
+ // See: https://github.com/facebook/jest/pull/10217 (fixed in v26.2.0)
398
+ // expect(gateway.load(mockApolloConfig)).rejects.toThrowErrorMatchingInlineSnapshot(`
399
+ // "A valid schema couldn't be composed. The following composition errors were found:
400
+ // [accounts] User -> A @key selects id, but User.id could not be found
401
+ // [accounts] Account -> A @key selects id, but Account.id could not be found"
402
+ // `);
403
+ // Instead we'll just use the regular snapshot matcher...
404
+ let err;
405
+ try {
406
+ await gateway.load(mockApolloConfig);
407
+ } catch (e) {
408
+ err = e;
409
+ }
410
+
411
+ // TODO: smell that we should be awaiting something else
412
+ expect(err.message).toMatchInlineSnapshot(`
413
+ "The gateway did not update its schema due to failed service health checks. The gateway will continue to operate with the previous schema and reattempt updates. The following error occurred during the health check:
414
+ [accounts]: 500: Internal Server Error"
415
+ `);
416
+
417
+ await expect(gateway.stop()).rejects.toThrowErrorMatchingInlineSnapshot(
418
+ `"ApolloGateway.stop does not need to be called before ApolloGateway.load is called successfully"`,
324
419
  );
420
+
421
+ // Set to `null` so we don't try to call `stop` on it in the `afterEach`,
422
+ // which triggers a different error that we're not testing for here.
423
+ gateway = null;
325
424
  });
326
425
  });
327
426
 
328
- describe.skip('Managed mode', () => {
427
+ describe('Managed mode', () => {
329
428
  it('Performs health checks to downstream services on load', async () => {
330
- mockStorageSecretSuccess();
331
- mockCompositionConfigLinkSuccess();
332
- mockCompositionConfigsSuccess([service]);
333
- mockImplementingServicesSuccess(service);
334
- mockRawPartialSchemaSuccess(service);
429
+ mockSupergraphSdlRequestSuccess();
430
+ mockAllServicesHealthCheckSuccess();
335
431
 
336
- mockServiceHealthCheckSuccess(service);
432
+ gateway = new ApolloGateway({
433
+ serviceHealthCheck: true,
434
+ logger,
435
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
436
+ });
437
+ // eslint-disable-next-line
438
+ // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
439
+ gateway.experimental_pollInterval = 100;
337
440
 
338
- const gateway = new ApolloGateway({ serviceHealthCheck: true, logger });
441
+ await gateway.load(mockApolloConfig);
442
+ await gateway.stop();
339
443
 
340
- await gateway.load({ engine: { apiKeyHash, graphId } });
341
- expect(gateway.schema!.getType('User')!.description).toBe('This is my User');
444
+ expect(gateway.schema!.getType('User')!).toBeTruthy();
342
445
  });
343
446
 
344
447
  it('Rejects on initial load when health check fails', async () => {
345
- mockStorageSecretSuccess();
346
- mockCompositionConfigLinkSuccess();
347
- mockCompositionConfigsSuccess([service]);
348
- mockImplementingServicesSuccess(service);
349
- mockRawPartialSchemaSuccess(service);
448
+ mockSupergraphSdlRequestSuccess();
449
+ mockServiceHealthCheck(accounts).reply(500);
450
+ mockServiceHealthCheckSuccess(books);
451
+ mockServiceHealthCheckSuccess(inventory);
452
+ mockServiceHealthCheckSuccess(product);
453
+ mockServiceHealthCheckSuccess(reviews);
454
+ mockServiceHealthCheckSuccess(documents);
455
+
456
+ gateway = new ApolloGateway({
457
+ serviceHealthCheck: true,
458
+ logger,
459
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
460
+ });
350
461
 
351
- mockServiceHealthCheck(service).reply(500);
462
+ // This is the ideal, but our version of Jest has a bug with printing error snapshots.
463
+ // See: https://github.com/facebook/jest/pull/10217 (fixed in v26.2.0)
464
+ // expect(gateway.load(mockApolloConfig)).rejects.toThrowErrorMatchingInlineSnapshot(`
465
+ // "A valid schema couldn't be composed. The following composition errors were found:
466
+ // [accounts] User -> A @key selects id, but User.id could not be found
467
+ // [accounts] Account -> A @key selects id, but Account.id could not be found"
468
+ // `);
469
+ // Instead we'll just use the regular snapshot matcher...
470
+ let err;
471
+ try {
472
+ await gateway.load(mockApolloConfig);
473
+ } catch (e) {
474
+ err = e;
475
+ }
476
+
477
+ // TODO: smell that we should be awaiting something else
478
+ expect(err.message).toMatchInlineSnapshot(`
479
+ "The gateway did not update its schema due to failed service health checks. The gateway will continue to operate with the previous schema and reattempt updates. The following error occurred during the health check:
480
+ [accounts]: 500: Internal Server Error"
481
+ `);
352
482
 
353
- const gateway = new ApolloGateway({ serviceHealthCheck: true, logger });
483
+ await expect(gateway.stop()).rejects.toThrowErrorMatchingInlineSnapshot(
484
+ `"ApolloGateway.stop does not need to be called before ApolloGateway.load is called successfully"`,
485
+ );
354
486
 
355
- await expect(
356
- gateway.load({ engine: { apiKeyHash, graphId } }),
357
- ).rejects.toThrowErrorMatchingInlineSnapshot(`"500: Internal Server Error"`);
487
+ // Set to `null` so we don't try to call `stop` on it in the `afterEach`,
488
+ // which triggers a different error that we're not testing for here.
489
+ gateway = null;
358
490
  });
359
491
 
360
492
  // This test has been flaky for a long time, and fails consistently after changes
361
493
  // introduced by https://github.com/apollographql/apollo-server/pull/4277.
362
494
  // I've decided to skip this test for now with hopes that we can one day
363
495
  // determine the root cause and test this behavior in a reliable manner.
364
- it.skip('Rolls over to new schema when health check succeeds', async () => {
365
- mockStorageSecretSuccess();
366
- mockCompositionConfigLinkSuccess();
367
- mockCompositionConfigsSuccess([service]);
368
- mockImplementingServicesSuccess(service);
369
- mockRawPartialSchemaSuccess(service);
370
- mockServiceHealthCheckSuccess(service);
496
+ it('Rolls over to new schema when health check succeeds', async () => {
497
+ mockSupergraphSdlRequestSuccess();
498
+ mockAllServicesHealthCheckSuccess();
371
499
 
372
500
  // Update
373
- mockStorageSecretSuccess();
374
- mockCompositionConfigLinkSuccess();
375
- mockCompositionConfigsSuccess([updatedService]);
376
- mockImplementingServicesSuccess(updatedService);
377
- mockRawPartialSchemaSuccess(updatedService);
378
- mockServiceHealthCheckSuccess(updatedService);
379
-
380
- let resolve1: () => void;
381
- let resolve2: () => void;
382
- const schemaChangeBlocker1 = new Promise(res => (resolve1 = res));
383
- const schemaChangeBlocker2 = new Promise(res => (resolve2 = res));
501
+ mockSupergraphSdlRequestSuccessIfAfter(
502
+ 'originalId-1234',
503
+ 'updatedId-5678',
504
+ getTestingSupergraphSdl(fixturesWithUpdate),
505
+ );
506
+ mockAllServicesHealthCheckSuccess();
507
+
508
+ let resolve1: GenericFunction;
509
+ let resolve2: GenericFunction;
510
+ const schemaChangeBlocker1 = new Promise((res) => (resolve1 = res));
511
+ const schemaChangeBlocker2 = new Promise((res) => (resolve2 = res));
384
512
  const onChange = jest
385
513
  .fn()
386
514
  .mockImplementationOnce(() => resolve1())
387
515
  .mockImplementationOnce(() => resolve2());
388
516
 
389
- const gateway = new ApolloGateway({
517
+ gateway = new ApolloGateway({
390
518
  serviceHealthCheck: true,
391
519
  logger,
520
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
392
521
  });
522
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
393
523
  // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
394
524
  gateway.experimental_pollInterval = 100;
395
525
 
396
526
  gateway.onSchemaChange(onChange);
397
- await gateway.load({ engine: { apiKeyHash, graphId } });
527
+ await gateway.load(mockApolloConfig);
398
528
 
529
+ // Basic testing schema doesn't contain a `review` field on `Query` type
399
530
  await schemaChangeBlocker1;
400
- expect(gateway.schema!.getType('User')!.description).toBe('This is my User');
531
+ expect(getRootQueryFields(gateway.schema)).not.toContain('review');
401
532
  expect(onChange).toHaveBeenCalledTimes(1);
402
533
 
534
+ // "Updated" testing schema adds a `review` field on `Query` type
403
535
  await schemaChangeBlocker2;
404
- expect(gateway.schema!.getType('User')!.description).toBe('This is my updated User');
536
+ expect(getRootQueryFields(gateway.schema)).toContain('review');
537
+
405
538
  expect(onChange).toHaveBeenCalledTimes(2);
406
539
  });
407
540
 
408
541
  it('Preserves original schema when health check fails', async () => {
409
- mockStorageSecretSuccess();
410
- mockCompositionConfigLinkSuccess();
411
- mockCompositionConfigsSuccess([service]);
412
- mockImplementingServicesSuccess(service);
413
- mockRawPartialSchemaSuccess(service);
414
- mockServiceHealthCheckSuccess(service);
415
-
416
- // Update
417
- mockStorageSecretSuccess();
418
- mockCompositionConfigLinkSuccess();
419
- mockCompositionConfigsSuccess([updatedService]);
420
- mockImplementingServicesSuccess(updatedService);
421
- mockRawPartialSchemaSuccess(updatedService);
422
- mockServiceHealthCheck(updatedService).reply(500);
542
+ mockSupergraphSdlRequestSuccess();
543
+ mockAllServicesHealthCheckSuccess();
544
+
545
+ // Update (with one health check failure)
546
+ mockSupergraphSdlRequestSuccessIfAfter(
547
+ 'originalId-1234',
548
+ 'updatedId-5678',
549
+ getTestingSupergraphSdl(fixturesWithUpdate),
550
+ );
551
+ mockServiceHealthCheck(accounts).reply(500);
552
+ mockServiceHealthCheckSuccess(books);
553
+ mockServiceHealthCheckSuccess(inventory);
554
+ mockServiceHealthCheckSuccess(product);
555
+ mockServiceHealthCheckSuccess(reviews);
556
+ mockServiceHealthCheckSuccess(documents);
423
557
 
424
- let resolve: () => void;
425
- const schemaChangeBlocker = new Promise(res => (resolve = res));
558
+ let resolve: GenericFunction;
559
+ const schemaChangeBlocker = new Promise((res) => (resolve = res));
426
560
 
427
- const gateway = new ApolloGateway({ serviceHealthCheck: true, logger });
561
+ gateway = new ApolloGateway({
562
+ serviceHealthCheck: true,
563
+ logger,
564
+ schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
565
+ });
566
+ // eslint-disable-next-line
428
567
  // @ts-ignore for testing purposes, a short pollInterval is ideal so we'll override here
429
568
  gateway.experimental_pollInterval = 100;
430
569
 
431
- // @ts-ignore for testing purposes, we'll call the original `updateComposition`
570
+ // eslint-disable-next-line
571
+ // @ts-ignore for testing purposes, we'll call the original `updateSchema`
432
572
  // function from our mock. The first call should mimic original behavior,
433
573
  // but the second call needs to handle the PromiseRejection. Typically for tests
434
574
  // like these we would leverage the `gateway.onSchemaChange` callback to drive
435
575
  // the test, but in this case, that callback isn't triggered when the update
436
576
  // fails (as expected) so we get creative with the second mock as seen below.
437
- const original = gateway.updateComposition;
438
- const mockUpdateComposition = jest
577
+ const original = gateway.updateSchema;
578
+ const mockUpdateSchema = jest
439
579
  .fn()
440
580
  .mockImplementationOnce(async () => {
441
581
  await original.apply(gateway);
@@ -443,30 +583,48 @@ describe('Downstream service health checks', () => {
443
583
  .mockImplementationOnce(async () => {
444
584
  // mock the first poll and handle the error which would otherwise be caught
445
585
  // and logged from within the `pollServices` class method
446
- await expect(original.apply(gateway))
447
- .rejects
448
- .toThrowErrorMatchingInlineSnapshot(
449
- `"500: Internal Server Error"`,
450
- );
586
+
587
+ // This is the ideal, but our version of Jest has a bug with printing error snapshots.
588
+ // See: https://github.com/facebook/jest/pull/10217 (fixed in v26.2.0)
589
+ // expect(original.apply(gateway)).rejects.toThrowErrorMatchingInlineSnapshot(`
590
+ // The gateway did not update its schema due to failed service health checks. The gateway will continue to operate with the previous schema and reattempt updates. The following error occurred during the health check:
591
+ // [accounts]: 500: Internal Server Error"
592
+ // `);
593
+ // Instead we'll just use the regular snapshot matcher...
594
+ let err;
595
+ try {
596
+ await original.apply(gateway);
597
+ } catch (e) {
598
+ err = e;
599
+ }
600
+
601
+ expect(err.message).toMatchInlineSnapshot(`
602
+ "The gateway did not update its schema due to failed service health checks. The gateway will continue to operate with the previous schema and reattempt updates. The following error occurred during the health check:
603
+ [accounts]: 500: Internal Server Error"
604
+ `);
451
605
  // finally resolve the promise which drives this test
452
606
  resolve();
453
607
  });
454
608
 
455
- // @ts-ignore for testing purposes, replace the `updateComposition`
609
+ // eslint-disable-next-line
610
+ // @ts-ignore for testing purposes, replace the `updateSchema`
456
611
  // function on the gateway with our mock
457
- gateway.updateComposition = mockUpdateComposition;
612
+ gateway.updateSchema = mockUpdateSchema;
458
613
 
459
614
  // load the gateway as usual
460
- await gateway.load({ engine: { apiKeyHash, graphId } });
615
+ await gateway.load(mockApolloConfig);
461
616
 
462
- expect(gateway.schema!.getType('User')!.description).toBe('This is my User');
617
+ // Validate we have the original schema
618
+ expect(getRootQueryFields(gateway.schema)).toContain('topReviews');
619
+ expect(getRootQueryFields(gateway.schema)).not.toContain('review');
463
620
 
464
621
  await schemaChangeBlocker;
465
622
 
466
623
  // At this point, the mock update should have been called but the schema
467
- // should not have updated to the new one.
468
- expect(mockUpdateComposition.mock.calls.length).toBe(2);
469
- expect(gateway.schema!.getType('User')!.description).toBe('This is my User');
624
+ // should still be the original.
625
+ expect(mockUpdateSchema).toHaveBeenCalledTimes(2);
626
+ expect(getRootQueryFields(gateway.schema)).toContain('topReviews');
627
+ expect(getRootQueryFields(gateway.schema)).not.toContain('review');
470
628
  });
471
629
  });
472
630
  });