@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.
- package/LICENSE +95 -0
- package/README.md +1 -1
- package/dist/__generated__/graphqlTypes.d.ts +130 -0
- package/dist/__generated__/graphqlTypes.d.ts.map +1 -0
- package/dist/__generated__/graphqlTypes.js +25 -0
- package/dist/__generated__/graphqlTypes.js.map +1 -0
- package/dist/config.d.ts +104 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/datasources/LocalGraphQLDataSource.d.ts +3 -3
- package/dist/datasources/LocalGraphQLDataSource.d.ts.map +1 -1
- package/dist/datasources/LocalGraphQLDataSource.js +5 -5
- package/dist/datasources/LocalGraphQLDataSource.js.map +1 -1
- package/dist/datasources/RemoteGraphQLDataSource.d.ts +6 -4
- package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
- package/dist/datasources/RemoteGraphQLDataSource.js +60 -17
- package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
- package/dist/datasources/index.d.ts +1 -1
- package/dist/datasources/index.d.ts.map +1 -1
- package/dist/datasources/index.js +1 -0
- package/dist/datasources/index.js.map +1 -1
- package/dist/datasources/parseCacheControlHeader.d.ts +2 -0
- package/dist/datasources/parseCacheControlHeader.d.ts.map +1 -0
- package/dist/datasources/parseCacheControlHeader.js +16 -0
- package/dist/datasources/parseCacheControlHeader.js.map +1 -0
- package/dist/datasources/types.d.ts +16 -1
- package/dist/datasources/types.d.ts.map +1 -1
- package/dist/datasources/types.js +7 -0
- package/dist/datasources/types.js.map +1 -1
- package/dist/executeQueryPlan.d.ts +2 -1
- package/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/executeQueryPlan.js +199 -112
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts +62 -80
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +543 -234
- package/dist/index.js.map +1 -1
- package/dist/loadServicesFromRemoteEndpoint.d.ts +9 -9
- package/dist/loadServicesFromRemoteEndpoint.d.ts.map +1 -1
- package/dist/loadServicesFromRemoteEndpoint.js +13 -8
- package/dist/loadServicesFromRemoteEndpoint.js.map +1 -1
- package/dist/loadSupergraphSdlFromStorage.d.ts +13 -0
- package/dist/loadSupergraphSdlFromStorage.d.ts.map +1 -0
- package/dist/loadSupergraphSdlFromStorage.js +101 -0
- package/dist/loadSupergraphSdlFromStorage.js.map +1 -0
- package/dist/operationContext.d.ts +17 -0
- package/dist/operationContext.d.ts.map +1 -0
- package/dist/operationContext.js +42 -0
- package/dist/operationContext.js.map +1 -0
- package/dist/outOfBandReporter.d.ts +15 -0
- package/dist/outOfBandReporter.d.ts.map +1 -0
- package/dist/outOfBandReporter.js +88 -0
- package/dist/outOfBandReporter.js.map +1 -0
- package/dist/utilities/array.d.ts +1 -2
- package/dist/utilities/array.d.ts.map +1 -1
- package/dist/utilities/array.js +7 -14
- package/dist/utilities/array.js.map +1 -1
- package/dist/utilities/assert.d.ts +2 -0
- package/dist/utilities/assert.d.ts.map +1 -0
- package/dist/utilities/assert.js +10 -0
- package/dist/utilities/assert.js.map +1 -0
- package/dist/utilities/cleanErrorOfInaccessibleNames.d.ts +3 -0
- package/dist/utilities/cleanErrorOfInaccessibleNames.d.ts.map +1 -0
- package/dist/utilities/cleanErrorOfInaccessibleNames.js +27 -0
- package/dist/utilities/cleanErrorOfInaccessibleNames.js.map +1 -0
- package/dist/utilities/deepMerge.js +2 -2
- package/dist/utilities/deepMerge.js.map +1 -1
- package/dist/utilities/graphql.d.ts +1 -4
- package/dist/utilities/graphql.d.ts.map +1 -1
- package/dist/utilities/graphql.js +3 -36
- package/dist/utilities/graphql.js.map +1 -1
- package/dist/utilities/opentelemetry.d.ts +10 -0
- package/dist/utilities/opentelemetry.d.ts.map +1 -0
- package/dist/utilities/opentelemetry.js +19 -0
- package/dist/utilities/opentelemetry.js.map +1 -0
- package/package.json +30 -21
- package/src/__generated__/graphqlTypes.ts +140 -0
- package/src/__mocks__/apollo-server-env.ts +56 -0
- package/src/__mocks__/make-fetch-happen-fetcher.ts +55 -0
- package/src/__mocks__/tsconfig.json +7 -0
- package/src/__tests__/build-query-plan.feature +40 -311
- package/src/__tests__/buildQueryPlan.test.ts +246 -426
- package/src/__tests__/executeQueryPlan.test.ts +1691 -194
- package/src/__tests__/execution-utils.ts +33 -26
- package/src/__tests__/gateway/__snapshots__/opentelemetry.test.ts.snap +195 -0
- package/src/__tests__/gateway/buildService.test.ts +16 -19
- package/src/__tests__/gateway/composedSdl.test.ts +44 -0
- package/src/__tests__/gateway/endToEnd.test.ts +166 -0
- package/src/__tests__/gateway/executor.test.ts +49 -43
- package/src/__tests__/gateway/lifecycle-hooks.test.ts +58 -29
- package/src/__tests__/gateway/opentelemetry.test.ts +123 -0
- package/src/__tests__/gateway/queryPlanCache.test.ts +19 -20
- package/src/__tests__/gateway/reporting.test.ts +76 -55
- package/src/__tests__/integration/abstract-types.test.ts +1086 -22
- package/src/__tests__/integration/aliases.test.ts +5 -6
- package/src/__tests__/integration/boolean.test.ts +40 -38
- package/src/__tests__/integration/complex-key.test.ts +41 -56
- package/src/__tests__/integration/configuration.test.ts +321 -0
- package/src/__tests__/integration/custom-directives.test.ts +61 -46
- package/src/__tests__/integration/fragments.test.ts +8 -2
- package/src/__tests__/integration/list-key.test.ts +2 -2
- package/src/__tests__/integration/logger.test.ts +2 -2
- package/src/__tests__/integration/multiple-key.test.ts +11 -12
- package/src/__tests__/integration/mutations.test.ts +8 -5
- package/src/__tests__/integration/networkRequests.test.ts +447 -289
- package/src/__tests__/integration/nockMocks.ts +95 -66
- package/src/__tests__/integration/provides.test.ts +9 -6
- package/src/__tests__/integration/requires.test.ts +17 -15
- package/src/__tests__/integration/scope.test.ts +557 -0
- package/src/__tests__/integration/unions.test.ts +1 -1
- package/src/__tests__/integration/value-types.test.ts +35 -32
- package/src/__tests__/integration/variables.test.ts +8 -2
- package/src/__tests__/loadServicesFromRemoteEndpoint.test.ts +6 -2
- package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +694 -0
- package/src/__tests__/queryPlanCucumber.test.ts +11 -61
- package/src/__tests__/testSetup.ts +1 -4
- package/src/__tests__/tsconfig.json +2 -1
- package/src/config.ts +225 -0
- package/src/core/__tests__/core.test.ts +412 -0
- package/src/datasources/LocalGraphQLDataSource.ts +9 -10
- package/src/datasources/RemoteGraphQLDataSource.ts +117 -43
- package/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +11 -4
- package/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +148 -79
- package/src/datasources/__tests__/tsconfig.json +4 -2
- package/src/datasources/index.ts +1 -1
- package/src/datasources/parseCacheControlHeader.ts +43 -0
- package/src/datasources/types.ts +47 -2
- package/src/executeQueryPlan.ts +264 -153
- package/src/index.ts +925 -480
- package/src/loadServicesFromRemoteEndpoint.ts +24 -17
- package/src/loadSupergraphSdlFromStorage.ts +140 -0
- package/src/make-fetch-happen.d.ts +2 -2
- package/src/operationContext.ts +70 -0
- package/src/outOfBandReporter.ts +128 -0
- package/src/utilities/__tests__/cleanErrorOfInaccessibleElements.test.ts +104 -0
- package/src/utilities/__tests__/tsconfig.json +8 -0
- package/src/utilities/array.ts +6 -28
- package/src/utilities/assert.ts +14 -0
- package/src/utilities/cleanErrorOfInaccessibleNames.ts +29 -0
- package/src/utilities/graphql.ts +0 -64
- package/src/utilities/opentelemetry.ts +13 -0
- package/CHANGELOG.md +0 -226
- package/LICENSE.md +0 -20
- package/dist/FieldSet.d.ts +0 -18
- package/dist/FieldSet.d.ts.map +0 -1
- package/dist/FieldSet.js +0 -96
- package/dist/FieldSet.js.map +0 -1
- package/dist/QueryPlan.d.ts +0 -41
- package/dist/QueryPlan.d.ts.map +0 -1
- package/dist/QueryPlan.js +0 -15
- package/dist/QueryPlan.js.map +0 -1
- package/dist/buildQueryPlan.d.ts +0 -44
- package/dist/buildQueryPlan.d.ts.map +0 -1
- package/dist/buildQueryPlan.js +0 -670
- package/dist/buildQueryPlan.js.map +0 -1
- package/dist/loadServicesFromStorage.d.ts +0 -21
- package/dist/loadServicesFromStorage.d.ts.map +0 -1
- package/dist/loadServicesFromStorage.js +0 -64
- package/dist/loadServicesFromStorage.js.map +0 -1
- package/dist/snapshotSerializers/astSerializer.d.ts +0 -3
- package/dist/snapshotSerializers/astSerializer.d.ts.map +0 -1
- package/dist/snapshotSerializers/astSerializer.js +0 -14
- package/dist/snapshotSerializers/astSerializer.js.map +0 -1
- package/dist/snapshotSerializers/index.d.ts +0 -13
- package/dist/snapshotSerializers/index.d.ts.map +0 -1
- package/dist/snapshotSerializers/index.js +0 -15
- package/dist/snapshotSerializers/index.js.map +0 -1
- package/dist/snapshotSerializers/queryPlanSerializer.d.ts +0 -3
- package/dist/snapshotSerializers/queryPlanSerializer.d.ts.map +0 -1
- package/dist/snapshotSerializers/queryPlanSerializer.js +0 -78
- package/dist/snapshotSerializers/queryPlanSerializer.js.map +0 -1
- package/dist/snapshotSerializers/selectionSetSerializer.d.ts +0 -3
- package/dist/snapshotSerializers/selectionSetSerializer.d.ts.map +0 -1
- package/dist/snapshotSerializers/selectionSetSerializer.js +0 -12
- package/dist/snapshotSerializers/selectionSetSerializer.js.map +0 -1
- package/dist/snapshotSerializers/typeSerializer.d.ts +0 -3
- package/dist/snapshotSerializers/typeSerializer.d.ts.map +0 -1
- package/dist/snapshotSerializers/typeSerializer.js +0 -12
- package/dist/snapshotSerializers/typeSerializer.js.map +0 -1
- package/dist/utilities/MultiMap.d.ts +0 -4
- package/dist/utilities/MultiMap.d.ts.map +0 -1
- package/dist/utilities/MultiMap.js +0 -17
- package/dist/utilities/MultiMap.js.map +0 -1
- package/src/FieldSet.ts +0 -169
- package/src/QueryPlan.ts +0 -57
- package/src/__tests__/matchers/toCallService.ts +0 -105
- package/src/__tests__/matchers/toHaveBeenCalledBefore.ts +0 -40
- package/src/__tests__/matchers/toHaveFetched.ts +0 -81
- package/src/__tests__/matchers/toMatchAST.ts +0 -64
- package/src/buildQueryPlan.ts +0 -1190
- package/src/loadServicesFromStorage.ts +0 -170
- package/src/snapshotSerializers/astSerializer.ts +0 -21
- package/src/snapshotSerializers/index.ts +0 -21
- package/src/snapshotSerializers/queryPlanSerializer.ts +0 -144
- package/src/snapshotSerializers/selectionSetSerializer.ts +0 -13
- package/src/snapshotSerializers/typeSerializer.ts +0 -11
- package/src/utilities/MultiMap.ts +0 -11
|
@@ -1,43 +1,43 @@
|
|
|
1
1
|
import nock from 'nock';
|
|
2
|
-
import
|
|
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
|
|
6
|
+
import { ApolloGateway } from '../..';
|
|
5
7
|
import {
|
|
6
|
-
|
|
8
|
+
mockSdlQuerySuccess,
|
|
7
9
|
mockServiceHealthCheckSuccess,
|
|
10
|
+
mockAllServicesHealthCheckSuccess,
|
|
8
11
|
mockServiceHealthCheck,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
partialSchemaPath: string;
|
|
32
|
+
name: string;
|
|
32
33
|
url: string;
|
|
33
|
-
|
|
34
|
+
typeDefs: DocumentNode;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
partialSchemaPath: 'accounts-partial-schema.json',
|
|
37
|
+
const simpleService: MockService = {
|
|
38
|
+
name: 'accounts',
|
|
39
39
|
url: 'http://localhost:4001',
|
|
40
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
97
|
+
mockSdlQuerySuccess(simpleService);
|
|
109
98
|
|
|
110
|
-
|
|
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('
|
|
119
|
-
|
|
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
|
-
|
|
107
|
+
gateway = new ApolloGateway({
|
|
108
|
+
logger,
|
|
109
|
+
schemaConfigDeliveryEndpoint: mockCloudConfigUrl,
|
|
110
|
+
});
|
|
126
111
|
|
|
127
|
-
await gateway.load(
|
|
128
|
-
|
|
112
|
+
await gateway.load(mockApolloConfig);
|
|
113
|
+
await gateway.stop();
|
|
114
|
+
expect(gateway.schema?.getType('User')).toBeTruthy();
|
|
129
115
|
});
|
|
130
116
|
|
|
131
|
-
it
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
300
|
-
mockServiceHealthCheckSuccess(
|
|
371
|
+
mockSdlQuerySuccess(simpleService);
|
|
372
|
+
mockServiceHealthCheckSuccess(simpleService);
|
|
301
373
|
|
|
302
|
-
|
|
374
|
+
gateway = new ApolloGateway({
|
|
303
375
|
logger,
|
|
304
|
-
serviceList: [
|
|
376
|
+
serviceList: [simpleService],
|
|
305
377
|
serviceHealthCheck: true,
|
|
306
378
|
});
|
|
307
379
|
|
|
308
380
|
await gateway.load();
|
|
309
|
-
expect(gateway.schema!.getType('User')!.description).toBe(
|
|
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
|
-
|
|
314
|
-
mockServiceHealthCheck(
|
|
387
|
+
mockSdlQuerySuccess(simpleService);
|
|
388
|
+
mockServiceHealthCheck(simpleService).reply(500);
|
|
315
389
|
|
|
316
|
-
|
|
317
|
-
serviceList: [
|
|
390
|
+
gateway = new ApolloGateway({
|
|
391
|
+
serviceList: [simpleService],
|
|
318
392
|
serviceHealthCheck: true,
|
|
319
393
|
logger,
|
|
320
394
|
});
|
|
321
395
|
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
427
|
+
describe('Managed mode', () => {
|
|
329
428
|
it('Performs health checks to downstream services on load', async () => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
mockCompositionConfigsSuccess([service]);
|
|
333
|
-
mockImplementingServicesSuccess(service);
|
|
334
|
-
mockRawPartialSchemaSuccess(service);
|
|
429
|
+
mockSupergraphSdlRequestSuccess();
|
|
430
|
+
mockAllServicesHealthCheckSuccess();
|
|
335
431
|
|
|
336
|
-
|
|
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
|
-
|
|
441
|
+
await gateway.load(mockApolloConfig);
|
|
442
|
+
await gateway.stop();
|
|
339
443
|
|
|
340
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
let resolve1:
|
|
381
|
-
let resolve2:
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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:
|
|
425
|
-
const schemaChangeBlocker = new Promise(res => (resolve = res));
|
|
558
|
+
let resolve: GenericFunction;
|
|
559
|
+
const schemaChangeBlocker = new Promise((res) => (resolve = res));
|
|
426
560
|
|
|
427
|
-
|
|
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
|
-
//
|
|
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.
|
|
438
|
-
const
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
//
|
|
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.
|
|
612
|
+
gateway.updateSchema = mockUpdateSchema;
|
|
458
613
|
|
|
459
614
|
// load the gateway as usual
|
|
460
|
-
await gateway.load(
|
|
615
|
+
await gateway.load(mockApolloConfig);
|
|
461
616
|
|
|
462
|
-
|
|
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
|
|
468
|
-
expect(
|
|
469
|
-
expect(gateway.schema
|
|
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
|
});
|