@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.
- 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 +106 -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 +64 -18
- 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 +200 -113
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts +64 -80
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +554 -233
- 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 +21 -0
- package/dist/loadSupergraphSdlFromStorage.d.ts.map +1 -0
- package/dist/loadSupergraphSdlFromStorage.js +128 -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 +13 -0
- package/dist/outOfBandReporter.d.ts.map +1 -0
- package/dist/outOfBandReporter.js +85 -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 +32 -23
- package/src/__generated__/graphqlTypes.ts +140 -0
- package/src/__mocks__/apollo-server-env.ts +56 -0
- package/src/__mocks__/make-fetch-happen-fetcher.ts +57 -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 +2289 -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 +83 -59
- 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 +361 -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 +454 -294
- package/src/__tests__/integration/nockMocks.ts +100 -65
- 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 +343 -0
- package/src/__tests__/nockAssertions.ts +20 -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 +227 -0
- package/src/core/__tests__/core.test.ts +412 -0
- package/src/datasources/LocalGraphQLDataSource.ts +9 -10
- package/src/datasources/RemoteGraphQLDataSource.ts +125 -45
- 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 +275 -154
- package/src/index.ts +939 -480
- package/src/loadServicesFromRemoteEndpoint.ts +24 -17
- package/src/loadSupergraphSdlFromStorage.ts +186 -0
- package/src/make-fetch-happen.d.ts +2 -2
- package/src/operationContext.ts +70 -0
- package/src/outOfBandReporter.ts +126 -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
|
-
import
|
|
2
|
-
import {
|
|
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
|
|
5
|
+
import { ApolloGateway } from '../..';
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
+
mockSdlQuerySuccess,
|
|
7
8
|
mockServiceHealthCheckSuccess,
|
|
9
|
+
mockAllServicesHealthCheckSuccess,
|
|
8
10
|
mockServiceHealthCheck,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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,38 +52,18 @@ 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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
96
|
+
mockSdlQuerySuccess(simpleService);
|
|
109
97
|
|
|
110
|
-
|
|
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('
|
|
119
|
-
|
|
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
|
-
|
|
106
|
+
gateway = new ApolloGateway({
|
|
107
|
+
logger,
|
|
108
|
+
uplinkEndpoints: [mockCloudConfigUrl1],
|
|
109
|
+
});
|
|
126
110
|
|
|
127
|
-
await gateway.load(
|
|
128
|
-
|
|
111
|
+
await gateway.load(mockApolloConfig);
|
|
112
|
+
await gateway.stop();
|
|
113
|
+
expect(gateway.schema?.getType('User')).toBeTruthy();
|
|
129
114
|
});
|
|
130
115
|
|
|
131
|
-
it
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
300
|
-
mockServiceHealthCheckSuccess(
|
|
373
|
+
mockSdlQuerySuccess(simpleService);
|
|
374
|
+
mockServiceHealthCheckSuccess(simpleService);
|
|
301
375
|
|
|
302
|
-
|
|
376
|
+
gateway = new ApolloGateway({
|
|
303
377
|
logger,
|
|
304
|
-
serviceList: [
|
|
378
|
+
serviceList: [simpleService],
|
|
305
379
|
serviceHealthCheck: true,
|
|
306
380
|
});
|
|
307
381
|
|
|
308
382
|
await gateway.load();
|
|
309
|
-
expect(gateway.schema!.getType('User')!.description).toBe(
|
|
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
|
-
|
|
314
|
-
mockServiceHealthCheck(
|
|
389
|
+
mockSdlQuerySuccess(simpleService);
|
|
390
|
+
mockServiceHealthCheck(simpleService).reply(500);
|
|
315
391
|
|
|
316
|
-
|
|
317
|
-
serviceList: [
|
|
392
|
+
gateway = new ApolloGateway({
|
|
393
|
+
serviceList: [simpleService],
|
|
318
394
|
serviceHealthCheck: true,
|
|
319
395
|
logger,
|
|
320
396
|
});
|
|
321
397
|
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
429
|
+
describe('Managed mode', () => {
|
|
329
430
|
it('Performs health checks to downstream services on load', async () => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
mockCompositionConfigsSuccess([service]);
|
|
333
|
-
mockImplementingServicesSuccess(service);
|
|
334
|
-
mockRawPartialSchemaSuccess(service);
|
|
431
|
+
mockSupergraphSdlRequestSuccess();
|
|
432
|
+
mockAllServicesHealthCheckSuccess();
|
|
335
433
|
|
|
336
|
-
|
|
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
|
-
|
|
443
|
+
await gateway.load(mockApolloConfig);
|
|
444
|
+
await gateway.stop();
|
|
339
445
|
|
|
340
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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));
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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:
|
|
425
|
-
const schemaChangeBlocker = new Promise(res => (resolve = res));
|
|
560
|
+
let resolve: GenericFunction;
|
|
561
|
+
const schemaChangeBlocker = new Promise((res) => (resolve = res));
|
|
426
562
|
|
|
427
|
-
|
|
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
|
-
//
|
|
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.
|
|
438
|
-
const
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
//
|
|
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.
|
|
614
|
+
gateway.updateSchema = mockUpdateSchema;
|
|
458
615
|
|
|
459
616
|
// load the gateway as usual
|
|
460
|
-
await gateway.load(
|
|
617
|
+
await gateway.load(mockApolloConfig);
|
|
461
618
|
|
|
462
|
-
|
|
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
|
|
468
|
-
expect(
|
|
469
|
-
expect(gateway.schema
|
|
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
|
});
|