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

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