console/integration-tests/tests/api/schema/external-composition.spec.ts
Laurin 6c6f5ab5f2
add Mutation.updateSchemaComposition to the public GraphQL API schema (#7635)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-09 23:22:57 +01:00

414 lines
13 KiB
TypeScript

import { ProjectType } from 'testkit/gql/graphql';
import { history } from '../../../testkit/external-composition';
import { updateSchemaComposition } from '../../../testkit/flow';
import { initSeed } from '../../../testkit/seed';
import { generateUnique, getServiceHost } from '../../../testkit/utils';
test.concurrent('call an external service to compose and validate services', async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, setNativeFederation } = await createProject(
ProjectType.Federation,
);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
const usersServiceName = generateUnique();
const publishUsersResult = await writeToken
.publishSchema({
url: 'https://api.com/users',
sdl: /* GraphQL */ `
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String
}
`,
service: usersServiceName,
})
.then(r => r.expectNoGraphQLErrors());
expect(publishUsersResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// expect `users` service to be composed internally
await expect(history()).resolves.not.toContainEqual(usersServiceName);
// we use internal docker network to connect to the external composition service,
// so we need to use the name and not resolved host
const dockerAddress = await getServiceHost('external_composition', 3012, false);
// enable external composition
const externalCompositionResult = await updateSchemaComposition(
{
project: {
bySelector: {
projectSlug: project.slug,
organizationSlug: organization.slug,
},
},
method: {
external: {
endpoint: `http://${dockerAddress}/compose`,
// eslint-disable-next-line no-process-env
secret: process.env.EXTERNAL_COMPOSITION_SECRET!,
},
},
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
expect(
externalCompositionResult.updateSchemaComposition.ok?.updatedProject.externalSchemaComposition
?.endpoint,
).toBe(`http://${dockerAddress}/compose`);
// set native federation to false to force external composition
await setNativeFederation(false);
const productsServiceName = generateUnique();
const publishProductsResult = await writeToken
.publishSchema({
url: 'https://api.com/products',
sdl: /* GraphQL */ `
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
type Query {
products: [Product]
}
type Product @key(fields: "id") {
id: ID!
name: String
}
`,
service: productsServiceName,
})
.then(r => r.expectNoGraphQLErrors());
// expect `products` service to be composed externally
await expect(history()).resolves.toContainEqual(productsServiceName);
// Schema publish should be successful
expect(publishProductsResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
});
test.concurrent(
'an expected error coming from the external composition service should be visible to the user',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, setNativeFederation, fetchVersions } =
await createProject(ProjectType.Federation);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
const usersServiceName = generateUnique();
const publishUsersResult = await writeToken
.publishSchema({
url: 'https://api.com/users',
sdl: /* GraphQL */ `
type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String
}
`,
service: usersServiceName,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishUsersResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// expect `users` service to be composed internally
await expect(history()).resolves.not.toContainEqual(usersServiceName);
// we use internal docker network to connect to the external composition service,
// so we need to use the name and not resolved host
const dockerAddress = await getServiceHost('external_composition', 3012, false);
// enable external composition
const externalCompositionResult = await updateSchemaComposition(
{
project: {
bySelector: {
projectSlug: project.slug,
organizationSlug: organization.slug,
},
},
method: {
external: {
endpoint: `http://${dockerAddress}/fail_on_signature`,
// eslint-disable-next-line no-process-env
secret: process.env.EXTERNAL_COMPOSITION_SECRET!,
},
},
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
expect(
externalCompositionResult.updateSchemaComposition.ok?.updatedProject.externalSchemaComposition
?.endpoint,
).toBe(`http://${dockerAddress}/fail_on_signature`);
// set native federation to false to force external composition
await setNativeFederation(false);
const productsServiceName = generateUnique();
const publishProductsResult = await writeToken
.publishSchema({
url: 'https://api.com/products',
sdl: /* GraphQL */ `
type Query {
products: [Product]
}
type Product @key(fields: "id") {
id: ID!
name: String
}
`,
service: productsServiceName,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be unsuccessful and the error coming from the external composition service should be visible
expect(publishProductsResult.schemaPublish).toEqual(
expect.objectContaining({
__typename: 'SchemaPublishError',
changes: {
total: 0,
nodes: [],
},
errors: {
total: 1,
nodes: [
{
message: expect.stringContaining('(ERR_INVALID_SIGNATURE)'), // composition
},
],
},
}),
);
// ensure no new schema version is created for failed external composition
const versions = await fetchVersions(20);
expect(versions.length).toEqual(1);
},
);
test.concurrent(
'a network error coming from the external composition service should be visible to the user',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, setNativeFederation, fetchVersions } =
await createProject(ProjectType.Federation);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
const usersServiceName = generateUnique();
const publishUsersResult = await writeToken
.publishSchema({
url: 'https://api.com/users',
sdl: /* GraphQL */ `
type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String
}
`,
service: usersServiceName,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishUsersResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// expect `users` service to be composed internally
await expect(history()).resolves.not.toContainEqual(usersServiceName);
// we use internal docker network to connect to the external composition service,
// so we need to use the name and not resolved host
const dockerAddress = await getServiceHost('external_composition', 3012, false);
// enable external composition
const externalCompositionResult = await updateSchemaComposition(
{
project: {
bySelector: {
projectSlug: project.slug,
organizationSlug: organization.slug,
},
},
method: {
external: {
endpoint: `http://${dockerAddress}/non-existing-endpoint`,
// eslint-disable-next-line no-process-env
secret: process.env.EXTERNAL_COMPOSITION_SECRET!,
},
},
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
expect(
externalCompositionResult.updateSchemaComposition.ok?.updatedProject.externalSchemaComposition
?.endpoint,
).toBe(`http://${dockerAddress}/non-existing-endpoint`);
// set native federation to false to force external composition
await setNativeFederation(false);
const productsServiceName = generateUnique();
const publishProductsResult = await writeToken
.publishSchema({
url: 'https://api.com/products',
sdl: /* GraphQL */ `
type Query {
products: [Product]
}
type Product @key(fields: "id") {
id: ID!
name: String
}
`,
service: productsServiceName,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be unsuccessful and the error coming from the external composition service should be visible
expect(publishProductsResult.schemaPublish).toEqual(
expect.objectContaining({
__typename: 'SchemaPublishError',
changes: {
total: 0,
nodes: [],
},
errors: {
total: 1,
nodes: [
{
message: expect.stringContaining('404'), // composition
},
],
},
}),
);
// ensure no new schema version is created for failed external composition
const versions = await fetchVersions(20);
expect(versions.length).toEqual(1);
},
);
test.concurrent('a timeout error should be visible to the user', async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, setNativeFederation, fetchVersions } =
await createProject(ProjectType.Federation);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
const usersServiceName = generateUnique();
const publishUsersResult = await writeToken
.publishSchema({
url: 'https://api.com/users',
sdl: /* GraphQL */ `
type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String
}
`,
service: usersServiceName,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishUsersResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// expect `users` service to be composed internally
await expect(history()).resolves.not.toContainEqual(usersServiceName);
// we use internal docker network to connect to the external composition service,
// so we need to use the name and not resolved host
const dockerAddress = await getServiceHost('external_composition', 3012, false);
// enable external composition
const externalCompositionResult = await updateSchemaComposition(
{
project: {
bySelector: {
projectSlug: project.slug,
organizationSlug: organization.slug,
},
},
method: {
external: {
endpoint: `http://${dockerAddress}/timeout`,
// eslint-disable-next-line no-process-env
secret: process.env.EXTERNAL_COMPOSITION_SECRET!,
},
},
},
ownerToken,
).then(r => r.expectNoGraphQLErrors());
expect(
externalCompositionResult.updateSchemaComposition.ok?.updatedProject.externalSchemaComposition
?.endpoint,
).toBe(`http://${dockerAddress}/timeout`);
// set native federation to false to force external composition
await setNativeFederation(false);
const productsServiceName = generateUnique();
const publishProductsResult = await writeToken
.publishSchema({
url: 'https://api.com/products',
sdl: /* GraphQL */ `
type Query {
products: [Product]
}
type Product @key(fields: "id") {
id: ID!
name: String
}
`,
service: productsServiceName,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be unsuccessful and the timeout error should be visible
expect(publishProductsResult.schemaPublish).toEqual(
expect.objectContaining({
__typename: 'SchemaPublishError',
changes: {
total: 0,
nodes: [],
},
errors: {
total: 1,
nodes: [
{
message: expect.stringMatching(/The schema composition timed out. Please try again./i),
},
],
},
linkToWebsite: null,
valid: false,
}),
);
// ensure no new schema version is created for failed external composition
const versions = await fetchVersions(20);
expect(versions.length).toEqual(1);
});