console/integration-tests/tests/api/schema/check.spec.ts

2631 lines
72 KiB
TypeScript

import {
ProjectType,
ResourceAssignmentModeType,
RuleInstanceSeverityLevel,
} from 'testkit/gql/graphql';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createStorage } from '@hive/storage';
import { graphql } from '../../../testkit/gql';
import { execute } from '../../../testkit/graphql';
import { initSeed } from '../../../testkit/seed';
import { createPolicy } from '../policy/policy-check.spec';
test.concurrent('can check a schema with target:registry:read access', async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
const noAccessToken = await createTargetAccessToken({
mode: 'noAccess',
});
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with no read and write rights
const checkResultErrors = await noAccessToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
foo: String
}
`)
.then(r => r.expectGraphQLErrors());
expect(checkResultErrors).toHaveLength(1);
expect(checkResultErrors[0].message).toMatch(
`No access (reason: "Missing permission for performing 'schemaCheck:create' on resource")`,
);
// Check schema with read rights
const checkResultValid = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
foo: String
}
`)
.then(r => r.expectNoGraphQLErrors());
expect(checkResultValid.schemaCheck.__typename).toBe('SchemaCheckSuccess');
});
test.concurrent('should match indentation of previous description', async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
" ping-ping "
ping: String
"pong-pong"
pong: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
"""
ping-ping
"""
ping: String
" pong-pong "
pong: String
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckSuccess') {
throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`);
}
expect(check.__typename).toBe('SchemaCheckSuccess');
expect(check.changes!.total).toBe(0);
});
const SchemaCheckQuery = graphql(/* GraphQL */ `
query SchemaCheckOnTargetQuery($selector: TargetSelectorInput!, $id: ID!) {
target(reference: { bySelector: $selector }) {
schemaCheck(id: $id) {
__typename
id
createdAt
schemaSDL
serviceName
schemaVersion {
id
}
meta {
commit
author
}
safeSchemaChanges {
nodes {
criticality
criticalityReason
message
path
approval {
schemaCheckId
approvedAt
approvedBy {
id
displayName
}
}
}
}
breakingSchemaChanges {
nodes {
criticality
criticalityReason
message
path
approval {
schemaCheckId
approvedAt
approvedBy {
id
displayName
}
}
}
}
schemaPolicyWarnings {
edges {
node {
message
ruleId
start {
line
column
}
end {
line
column
}
}
}
}
... on SuccessfulSchemaCheck {
compositeSchemaSDL
supergraphSDL
approvalComment
}
... on FailedSchemaCheck {
compositionErrors {
nodes {
message
path
}
}
schemaPolicyErrors {
edges {
node {
message
ruleId
start {
line
column
}
end {
line
column
}
}
}
}
}
}
}
}
`);
const ApproveFailedSchemaCheckMutation = graphql(/* GraphQL */ `
mutation ApproveFailedSchemaCheck($input: ApproveFailedSchemaCheckInput!) {
approveFailedSchemaCheck(input: $input) {
ok {
schemaCheck {
__typename
... on SuccessfulSchemaCheck {
isApproved
approvalComment
approvedBy {
__typename
}
}
}
}
error {
message
}
}
}
`);
test.concurrent(
'successful check without previously published schema is persisted',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
pong: String
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckSuccess') {
throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
id: schemaCheckId,
createdAt: expect.any(String),
serviceName: null,
schemaVersion: null,
},
},
});
},
);
test.concurrent(
'successful check with previously published schema is persisted',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
pong: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
pong: String
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckSuccess') {
throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
id: schemaCheckId,
createdAt: expect.any(String),
serviceName: null,
schemaVersion: {
id: expect.any(String),
},
},
},
});
},
);
test.concurrent('failed check due to graphql validation is persisted', async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Str
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'FailedSchemaCheck',
id: schemaCheckId,
createdAt: expect.any(String),
serviceName: null,
schemaVersion: null,
compositionErrors: {
nodes: [
{
message: 'Unknown type "Str".',
},
],
},
},
},
});
});
test.concurrent('failed check due to breaking change is persisted', async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'FailedSchemaCheck',
id: schemaCheckId,
createdAt: expect.any(String),
serviceName: null,
schemaVersion: {
id: expect.any(String),
},
compositionErrors: null,
breakingSchemaChanges: {
nodes: [
{
message: "Field 'Query.ping' changed type from 'String' to 'Float'",
},
],
},
},
},
});
});
test.concurrent('failed check due to policy error is persisted', async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target, setProjectSchemaPolicy } = await createProject(
ProjectType.Single,
);
await setProjectSchemaPolicy(createPolicy(RuleInstanceSeverityLevel.Error));
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
foo: String
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'FailedSchemaCheck',
id: schemaCheckId,
createdAt: expect.any(String),
serviceName: null,
schemaVersion: {
id: expect.any(String),
},
compositionErrors: null,
breakingSchemaChanges: null,
schemaPolicyErrors: {
edges: [
{
node: {
end: {
column: 17,
line: 2,
},
message: 'Description is required for type "Query"',
ruleId: 'require-description',
start: {
column: 12,
line: 2,
},
},
},
],
},
},
},
});
});
test.concurrent(
'successful check with warnings and safe changes is persisted',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target, setProjectSchemaPolicy } =
await createProject(ProjectType.Single);
await setProjectSchemaPolicy(createPolicy(RuleInstanceSeverityLevel.Warning));
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
foo: String
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckSuccess') {
throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
id: schemaCheckId,
createdAt: expect.any(String),
serviceName: null,
schemaVersion: {
id: expect.any(String),
},
schemaPolicyWarnings: {
edges: [
{
node: {
end: {
column: expect.any(Number),
line: expect.any(Number),
},
message: 'Description is required for type "Query"',
ruleId: 'require-description',
start: {
column: expect.any(Number),
line: expect.any(Number),
},
},
},
],
},
safeSchemaChanges: {
nodes: [
{
message: "Field 'foo' was added to object type 'Query'",
},
],
},
},
},
});
},
);
test.concurrent(
'failed check due to missing service name is not persisted (federation/stitching)',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject(ProjectType.Federation);
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
foo: String
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
expect(check.schemaCheck).toEqual(null);
},
);
test.concurrent('metadata is persisted', async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: String
pong: String
}
`,
undefined,
{
author: 'Freddy Gibbs',
commit: '$oul $old $eparately',
},
)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckSuccess') {
throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
id: schemaCheckId,
createdAt: expect.any(String),
serviceName: null,
schemaVersion: null,
meta: {
author: 'Freddy Gibbs',
commit: '$oul $old $eparately',
},
},
},
});
});
test.concurrent(
'approve failed schema check that has breaking change status to successful and attaches meta information to the breaking change',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const mutationResult = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult).toEqual({
approveFailedSchemaCheck: {
ok: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
isApproved: true,
approvalComment: null,
approvedBy: {
__typename: 'User',
},
},
},
error: null,
},
});
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
breakingSchemaChanges: {
nodes: [
{
approval: {
schemaCheckId,
approvedAt: expect.any(String),
approvedBy: {
id: expect.any(String),
displayName: expect.any(String),
},
},
},
],
},
},
},
});
},
);
test.concurrent('approve failed schema check with a comment', async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
// Check schema with read rights
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const mutationResult = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
comment: 'This is a comment',
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult).toEqual({
approveFailedSchemaCheck: {
ok: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
isApproved: true,
approvalComment: 'This is a comment',
approvedBy: {
__typename: 'User',
},
},
},
error: null,
},
});
const schemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: schemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(schemaCheck).toMatchObject({
target: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
approvalComment: 'This is a comment',
},
},
});
});
test.concurrent(
'approving a schema check with contextId containing breaking changes allows the changes for subsequent checks with the same contextId',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
const contextId = 'pr-69420';
// Check schema with read rights
const checkResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: Float
}
`,
undefined,
undefined,
contextId,
)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const mutationResult = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult).toEqual({
approveFailedSchemaCheck: {
ok: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
isApproved: true,
approvalComment: null,
approvedBy: {
__typename: 'User',
},
},
},
error: null,
},
});
const secondCheckResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: Float
}
`,
undefined,
undefined,
contextId,
)
.then(r => r.expectNoGraphQLErrors());
if (secondCheckResult.schemaCheck.__typename !== 'SchemaCheckSuccess') {
throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`);
}
const newSchemaCheckId = secondCheckResult.schemaCheck.schemaCheck?.id;
if (newSchemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const newSchemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: newSchemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(newSchemaCheck.target?.schemaCheck).toMatchObject({
id: newSchemaCheckId,
breakingSchemaChanges: {
nodes: [
{
approval: {
schemaCheckId,
approvedAt: expect.any(String),
approvedBy: {
id: expect.any(String),
displayName: expect.any(String),
},
},
},
],
},
});
},
);
test.concurrent(
'approving a schema check with contextId containing breaking changes does not allow the changes for subsequent checks with a different contextId',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
const contextId = 'pr-69420';
// Check schema with read rights
const checkResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: Float
}
`,
undefined,
undefined,
contextId,
)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const mutationResult = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult).toEqual({
approveFailedSchemaCheck: {
ok: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
isApproved: true,
approvalComment: null,
approvedBy: {
__typename: 'User',
},
},
},
error: null,
},
});
const secondCheckResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: Float
}
`,
undefined,
undefined,
contextId + '|' + contextId,
)
.then(r => r.expectNoGraphQLErrors());
if (secondCheckResult.schemaCheck.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`);
}
const newSchemaCheckId = secondCheckResult.schemaCheck.schemaCheck?.id;
if (newSchemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const newSchemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: newSchemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(newSchemaCheck.target?.schemaCheck).toMatchObject({
id: newSchemaCheckId,
breakingSchemaChanges: {
nodes: [
{
approval: null,
},
],
},
});
},
);
test.concurrent(
'can not approve schema check with insufficient permissions granted by default user role',
async () => {
const { createOrg } = await initSeed().createOwner();
const { organization, createProject, inviteAndJoinMember } = await createOrg();
// Setup Start: Create a failed schema check
const { project, createTargetAccessToken, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
const checkResult = await writeToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
if (checkResult.schemaCheck.__typename !== 'SchemaCheckError') {
throw new Error('Invalid result: ' + checkResult.schemaCheck.__typename);
}
const schemaCheckId = await checkResult.schemaCheck.schemaCheck?.id;
if (!schemaCheckId) {
throw new Error('Invalid result: ' + JSON.stringify(checkResult, null, 2));
}
// Setup Done: Create a failed schema check
// Create a member with no access to projects
const { member, assignMemberRole, memberToken } = await inviteAndJoinMember();
expect(member.role.name).toEqual('Viewer');
await assignMemberRole({
roleId: member.role.id,
userId: member.user.id,
resources: { mode: ResourceAssignmentModeType.Granular, projects: [] },
});
// Attempt approving the failed schema check
const errors = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: memberToken,
}).then(r => r.expectGraphQLErrors());
expect(errors).toHaveLength(1);
expect(errors.at(0)).toMatchObject({
extensions: {
code: 'UNAUTHORISED',
},
message: `No access (reason: "Missing permission for performing 'schemaCheck:approve' on resource")`,
path: ['approveFailedSchemaCheck'],
});
},
);
test.concurrent(
'can not approve schema check with insufficient permissions granted by member role (no access to project resource)',
async () => {
const { createOrg } = await initSeed().createOwner();
const { organization, createProject, inviteAndJoinMember } = await createOrg();
// Setup Start: Create a failed schema check
const { project, createTargetAccessToken, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
const checkResult = await writeToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
if (checkResult.schemaCheck.__typename !== 'SchemaCheckError') {
throw new Error('Invalid result: ' + checkResult.schemaCheck.__typename);
}
const schemaCheckId = await checkResult.schemaCheck.schemaCheck?.id;
if (!schemaCheckId) {
throw new Error('Invalid result: ' + JSON.stringify(checkResult, null, 2));
}
// Setup Done: Create a failed schema check
// Create a member with no access to projects
const { member, assignMemberRole, createMemberRole, memberToken } = await inviteAndJoinMember();
const memberRole = await createMemberRole(['schemaCheck:approve', 'project:describe']);
await assignMemberRole({
roleId: memberRole.id,
userId: member.user.id,
resources: { mode: ResourceAssignmentModeType.Granular, projects: [] },
});
// Attempt approving the failed schema check
const errors = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: memberToken,
}).then(r => r.expectGraphQLErrors());
expect(errors).toHaveLength(1);
expect(errors.at(0)).toMatchObject({
extensions: {
code: 'UNAUTHORISED',
},
message: `No access (reason: "Missing permission for performing 'schemaCheck:approve' on resource")`,
path: ['approveFailedSchemaCheck'],
});
},
);
test.concurrent(
'can not approve schema check with insufficient permissions granted by member role (no access to target resource)',
async () => {
const { createOrg } = await initSeed().createOwner();
const { organization, createProject, inviteAndJoinMember } = await createOrg();
// Setup Start: Create a failed schema check
const { project, createTargetAccessToken, target, targets } = await createProject(
ProjectType.Single,
);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
const checkResult = await writeToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
if (checkResult.schemaCheck.__typename !== 'SchemaCheckError') {
throw new Error('Invalid result: ' + checkResult.schemaCheck.__typename);
}
const schemaCheckId = await checkResult.schemaCheck.schemaCheck?.id;
if (!schemaCheckId) {
throw new Error('Invalid result: ' + JSON.stringify(checkResult, null, 2));
}
// Setup Done: Create a failed schema check
// Create a member with no access to project targets
const { member, createMemberRole, assignMemberRole, memberToken } = await inviteAndJoinMember();
const memberRole = await createMemberRole(['schemaCheck:approve', 'project:describe']);
await assignMemberRole({
roleId: memberRole.id,
userId: member.user.id,
resources: {
mode: ResourceAssignmentModeType.Granular,
projects: [
{
projectId: project.id,
targets: {
mode: ResourceAssignmentModeType.Granular,
targets: [],
},
},
],
},
});
// Attempt approving the failed schema check
const errors = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: memberToken,
}).then(r => r.expectGraphQLErrors());
expect(errors).toHaveLength(1);
expect(errors.at(0)).toMatchObject({
extensions: {
code: 'UNAUTHORISED',
},
message: `No access (reason: "Missing permission for performing 'schemaCheck:approve' on resource")`,
path: ['approveFailedSchemaCheck'],
});
},
);
test.concurrent(
'can approve schema with sufficient permissions granted by member role (access to target)',
async () => {
const { createOrg } = await initSeed().createOwner();
const { organization, createProject, inviteAndJoinMember } = await createOrg();
// Setup Start: Create a failed schema check
const { project, createTargetAccessToken, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
const checkResult = await writeToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
if (checkResult.schemaCheck.__typename !== 'SchemaCheckError') {
throw new Error('Invalid result: ' + checkResult.schemaCheck.__typename);
}
const schemaCheckId = await checkResult.schemaCheck.schemaCheck?.id;
if (!schemaCheckId) {
throw new Error('Invalid result: ' + JSON.stringify(checkResult, null, 2));
}
// Setup Done: Create a failed schema check
// Create a member with no access to projects
const { member, createMemberRole, assignMemberRole, memberToken } = await inviteAndJoinMember();
const memberRole = await createMemberRole(['schemaCheck:approve', 'project:describe']);
await assignMemberRole({
roleId: memberRole.id,
userId: member.user.id,
resources: {
mode: ResourceAssignmentModeType.Granular,
projects: [
{
projectId: project.id,
targets: {
mode: ResourceAssignmentModeType.Granular,
targets: [
{
targetId: target.id,
appDeployments: { mode: ResourceAssignmentModeType.All },
services: { mode: ResourceAssignmentModeType.All },
},
],
},
},
],
},
});
// Attempt approving the failed schema check
const result = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: memberToken,
}).then(r => r.expectNoGraphQLErrors());
expect(result.approveFailedSchemaCheck.ok).toMatchObject({
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
isApproved: true,
},
});
},
);
test.concurrent(
'subsequent schema check with shared contextId that contains new breaking changes that have not been approved fails',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
pong: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
const contextId = 'pr-69420';
// Check schema with read rights
const checkResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: Float
}
`,
undefined,
undefined,
contextId,
)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const mutationResult = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult).toEqual({
approveFailedSchemaCheck: {
ok: {
schemaCheck: {
__typename: 'SuccessfulSchemaCheck',
isApproved: true,
approvalComment: null,
approvedBy: {
__typename: 'User',
},
},
},
error: null,
},
});
const secondCheckResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: Float
pong: Float
}
`,
undefined,
undefined,
contextId,
)
.then(r => r.expectNoGraphQLErrors());
if (secondCheckResult.schemaCheck.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`);
}
const newSchemaCheckId = secondCheckResult.schemaCheck.schemaCheck?.id;
if (newSchemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const newSchemaCheck = await execute({
document: SchemaCheckQuery,
variables: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
id: newSchemaCheckId,
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(newSchemaCheck.target?.schemaCheck).toMatchObject({
id: newSchemaCheckId,
breakingSchemaChanges: {
nodes: [
{
approval: {
schemaCheckId,
approvedAt: expect.any(String),
approvedBy: {
id: expect.any(String),
displayName: expect.any(String),
},
},
},
{
approval: null,
},
],
},
});
},
);
test.concurrent(
'contextId that has more than 300 characters is not allowed',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
pong: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
const contextId = '';
// Check schema with read rights
const checkResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: Float
}
`,
undefined,
undefined,
contextId,
)
.then(r => r.expectNoGraphQLErrors());
expect(checkResult.schemaCheck).toMatchObject({
__typename: 'SchemaCheckError',
errors: {
nodes: [
{
message: 'Context ID must be at least 1 character long.',
},
],
},
});
},
);
test.concurrent('contextId that has fewer than 1 characters is not allowed', async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject(ProjectType.Single);
// Create a token with write rights
const writeToken = await createTargetAccessToken({});
// Publish schema with write rights
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
pong: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
// Schema publish should be successful
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
// Create a token with read rights
const readToken = await createTargetAccessToken({
mode: 'readOnly',
});
const contextId = new Array(201).fill('A').join('');
// Check schema with read rights
const checkResult = await readToken
.checkSchema(
/* GraphQL */ `
type Query {
ping: Float
}
`,
undefined,
undefined,
contextId,
)
.then(r => r.expectNoGraphQLErrors());
expect(checkResult.schemaCheck).toMatchObject({
__typename: 'SchemaCheckError',
errors: {
nodes: [
{
message: 'Context ID cannot exceed length of 200 characters.',
},
],
},
});
});
describe.concurrent(
'schema check composition skip due to unchanged input schemas when being compared to failed schema version',
() => {
test.concurrent('native federation', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
const { createTargetAccessToken, setNativeFederation } = await createProject(
ProjectType.Federation,
);
await setFeatureFlag('compareToPreviousComposableVersion', true);
await setNativeFederation(true);
const token = await createTargetAccessToken({});
// here we use @tag without an argument to trigger a validation/composition error
const sdl = /* GraphQL */ `
extend schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"])
type Query {
ping: String
pong: String
foo: User @tag
}
type User {
id: ID!
}
`;
// Publish schema with write rights
await token
.publishSchema({
sdl,
service: 'serviceA',
url: 'http://localhost:4000',
})
.then(r => r.expectNoGraphQLErrors());
const result = await token.checkSchema(sdl, 'serviceA').then(r => r.expectNoGraphQLErrors());
expect(result.schemaCheck).toMatchObject({
valid: false,
__typename: 'SchemaCheckError',
changes: expect.objectContaining({
total: 0,
}),
errors: expect.objectContaining({
total: 1,
}),
});
});
test.concurrent('legacy fed composition', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
const { createTargetAccessToken, setNativeFederation } = await createProject(
ProjectType.Federation,
);
await setFeatureFlag('compareToPreviousComposableVersion', false);
await setNativeFederation(false);
const token = await createTargetAccessToken({});
// @key(fields:) is invalid - should trigger a composition error
const sdl = /* GraphQL */ `
type Query {
ping: String
pong: String
foo: User
}
type User @key(fields: "uuid") {
id: ID!
}
`;
// Publish schema with write rights
await token
.publishSchema({
sdl,
service: 'serviceA',
url: 'http://localhost:4000',
})
.then(r => r.expectNoGraphQLErrors());
const result = await token.checkSchema(sdl, 'serviceA').then(r => r.expectNoGraphQLErrors());
expect(result.schemaCheck).toMatchObject({
valid: false,
__typename: 'SchemaCheckError',
changes: expect.objectContaining({
total: 0,
}),
errors: expect.objectContaining({
total: 1,
}),
});
});
test.concurrent(
'legacy fed composition with compareToPreviousComposableVersion=true',
async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
const { createTargetAccessToken, setNativeFederation } = await createProject(
ProjectType.Federation,
);
await setFeatureFlag('compareToPreviousComposableVersion', true);
await setNativeFederation(false);
const token = await createTargetAccessToken({});
// @key(fields:) is invalid - should trigger a composition error
const sdl = /* GraphQL */ `
type Query {
ping: String
pong: String
foo: User
}
type User @key(fields: "uuid") {
id: ID!
}
`;
// Publish schema with write rights
await token
.publishSchema({
sdl,
service: 'serviceA',
url: 'http://localhost:4000',
})
.then(r => r.expectNoGraphQLErrors());
const result = await token
.checkSchema(sdl, 'serviceA')
.then(r => r.expectNoGraphQLErrors());
expect(result.schemaCheck).toMatchObject({
valid: false,
__typename: 'SchemaCheckError',
changes: expect.objectContaining({
total: 0,
}),
errors: expect.objectContaining({
total: 1,
}),
});
},
);
},
);
test.concurrent(
'checking an invalid schema fails due to validation errors (deprecated non-nullable input field)',
async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject(ProjectType.Single);
const token = await createTargetAccessToken({});
const sdl = /* GraphQL */ `
type Query {
a(b: B!): String
}
input B {
a: String! @deprecated(reason: "This field is deprecated")
b: String!
}
`;
const result = await token.checkSchema(sdl).then(r => r.expectNoGraphQLErrors());
expect(result.schemaCheck).toEqual({
__typename: 'SchemaCheckError',
changes: {
nodes: [],
total: 0,
},
errors: {
nodes: [
{
message: 'Required input field B.a cannot be deprecated.',
},
],
total: 1,
},
schemaCheck: {
id: expect.any(String),
},
valid: false,
});
},
);
function connectionString() {
const {
POSTGRES_USER = 'postgres',
POSTGRES_PASSWORD = 'postgres',
POSTGRES_HOST = 'localhost',
POSTGRES_PORT = 5432,
POSTGRES_DB = 'registry',
POSTGRES_SSL = null,
POSTGRES_CONNECTION_STRING = null,
} = process.env;
return (
POSTGRES_CONNECTION_STRING ||
`postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}${
POSTGRES_SSL ? '?sslmode=require' : '?sslmode=disable'
}`
);
}
test.concurrent(
'checking a valid schema onto a broken schema succeeds (prior schema has deprecated non-nullable input)',
async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
const token = await createTargetAccessToken({});
const brokenSdl = /* GraphQL */ `
type Query {
a(b: B!): String
}
input B {
a: String! @deprecated(reason: "This field is deprecated")
b: String!
}
`;
// we need to manually insert a broken schema version into the database
// as we fixed the issue that allows publishing such a broken version
const conn = connectionString();
const storage = await createStorage(conn, 2);
await storage.createVersion({
schema: brokenSdl,
author: 'Jochen',
async actionFn() {},
base_schema: null,
commit: '123',
changes: [],
compositeSchemaSDL: null,
conditionalBreakingChangeMetadata: null,
contracts: null,
coordinatesDiff: null,
diffSchemaVersionId: null,
github: null,
metadata: null,
logIds: [],
projectId: project.id,
service: null,
organizationId: organization.id,
previousSchemaVersion: null,
valid: true,
schemaCompositionErrors: [],
supergraphSDL: null,
tags: null,
targetId: target.id,
url: null,
schemaMetadata: null,
metadataAttributes: null,
});
await storage.destroy();
const validSdl = /* GraphQL */ `
type Query {
a(b: B!): String
}
input B {
a: String @deprecated(reason: "This field is deprecated")
b: String!
}
`;
const sdl = /* GraphQL */ `
type Query {
a(b: B!): String
}
input B {
a: String @deprecated(reason: "This field is deprecated")
b: String!
}
`;
const result = await token.checkSchema(sdl).then(r => r.expectNoGraphQLErrors());
expect(result.schemaCheck).toEqual({
__typename: 'SchemaCheckSuccess',
changes: {
nodes: [
{
criticality: 'Safe',
message: "Input field 'B.a' changed type from 'String!' to 'String'",
},
],
total: 1,
},
schemaCheck: {
id: expect.any(String),
},
valid: true,
});
},
);
test.concurrent('schema policy errors prevent schema check approval', async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, setProjectSchemaPolicy, project, target } = await createProject(
ProjectType.Single,
);
await setProjectSchemaPolicy(createPolicy(RuleInstanceSeverityLevel.Error));
const writeToken = await createTargetAccessToken({});
await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
const checkResult = await writeToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const mutationResult = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult).toEqual({
approveFailedSchemaCheck: {
ok: null,
error: {
message: 'Schema check has schema policy errors that must be resolved before approval.',
},
},
});
});
test.concurrent(
'schema policy errors prevent schema check approval with safe changes',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, setProjectSchemaPolicy, project, target } =
await createProject(ProjectType.Single);
await setProjectSchemaPolicy(createPolicy(RuleInstanceSeverityLevel.Error));
const writeToken = await createTargetAccessToken({});
await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
const checkResult = await writeToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
pong: String
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const mutationResult = await execute({
document: ApproveFailedSchemaCheckMutation,
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
},
authToken: ownerToken,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult).toEqual({
approveFailedSchemaCheck: {
ok: null,
error: {
message: 'Schema check has schema policy errors that must be resolved before approval.',
},
},
});
},
);
test.concurrent(
'approve failed schema check with author field using target access token',
async ({ expect }) => {
const { createOrg } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
const writeToken = await createTargetAccessToken({});
const publishResult = await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess');
const readToken = await createTargetAccessToken({
mode: 'readWrite',
});
const checkResult = await readToken
.checkSchema(/* GraphQL */ `
type Query {
ping: Float
}
`)
.then(r => r.expectNoGraphQLErrors());
const check = checkResult.schemaCheck;
if (check.__typename !== 'SchemaCheckError') {
throw new Error(`Expected SchemaCheckError, got ${check.__typename}`);
}
const schemaCheckId = check.schemaCheck?.id;
if (schemaCheckId == null) {
throw new Error('Missing schema check id.');
}
const mutationResult = await execute({
document: graphql(/* GraphQL */ `
mutation ApproveFailedSchemaCheckWithAuthor($input: ApproveFailedSchemaCheckInput!) {
approveFailedSchemaCheck(input: $input) {
ok {
schemaCheck {
__typename
... on SuccessfulSchemaCheck {
isApproved
approvalComment
cliApprovalMetadata {
displayName
email
}
}
}
}
error {
message
}
}
}
`),
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
comment: 'Check force approved automatically via CLI --forceSafe flag',
author: 'John Doe <john@example.com>',
},
},
authToken: readToken.secret,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult.approveFailedSchemaCheck.ok).not.toBeNull();
expect(mutationResult.approveFailedSchemaCheck.error).toBeNull();
const approvedCheck = mutationResult.approveFailedSchemaCheck.ok?.schemaCheck;
expect(approvedCheck?.__typename).toBe('SuccessfulSchemaCheck');
if (approvedCheck?.__typename === 'SuccessfulSchemaCheck') {
expect(approvedCheck.isApproved).toBe(true);
expect(approvedCheck.cliApprovalMetadata).toMatchObject({
displayName: 'John Doe',
email: 'john@example.com',
});
}
const schemaCheckQueryResult = await execute({
document: graphql(/* GraphQL */ `
query GetSchemaCheckWithApproval(
$organizationSlug: String!
$projectSlug: String!
$targetSlug: String!
$schemaCheckId: ID!
) {
target(
reference: {
bySelector: {
organizationSlug: $organizationSlug
projectSlug: $projectSlug
targetSlug: $targetSlug
}
}
) {
schemaCheck(id: $schemaCheckId) {
__typename
... on SuccessfulSchemaCheck {
id
isApproved
cliApprovalMetadata {
displayName
email
}
breakingSchemaChanges {
nodes {
message
approval {
cliApprovalMetadata {
displayName
email
}
}
}
}
}
}
}
}
`),
variables: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId,
},
authToken: readToken.secret,
}).then(r => r.expectNoGraphQLErrors());
const queriedCheck = schemaCheckQueryResult.target?.schemaCheck;
expect(queriedCheck?.__typename).toBe('SuccessfulSchemaCheck');
if (queriedCheck?.__typename === 'SuccessfulSchemaCheck') {
expect(queriedCheck.isApproved).toBe(true);
const breakingChanges = queriedCheck.breakingSchemaChanges?.nodes ?? [];
expect(breakingChanges.length).toBeGreaterThan(0);
for (const change of breakingChanges) {
expect(change.approval?.cliApprovalMetadata).toMatchObject({
displayName: 'John Doe',
email: 'john@example.com',
});
}
}
},
);
test.concurrent(
'approve failed schema check handles different author formats',
async ({ expect }) => {
const testCases = [
{
author: 'john@example.com',
expected: { displayName: 'john@example.com', email: 'john@example.com' },
description: 'email only',
},
{
author: 'John Doe',
expected: { displayName: 'John Doe', email: '<no email provided>' },
description: 'name only',
},
{
author: 'John Doe <john@example.com>',
expected: { displayName: 'John Doe', email: 'john@example.com' },
description: 'git standard format',
},
];
for (const testCase of testCases) {
const { createOrg } = await initSeed().createOwner();
const { createProject, organization } = await createOrg();
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Single);
const writeToken = await createTargetAccessToken({
mode: 'readWrite',
});
await writeToken
.publishSchema({
sdl: /* GraphQL */ `
type Query {
ping: String
email: String
}
`,
})
.then(r => r.expectNoGraphQLErrors());
const checkResult = await writeToken
.checkSchema(/* GraphQL */ `
type Query {
ping: String
}
`)
.then(r => r.expectNoGraphQLErrors());
expect(checkResult.schemaCheck.__typename).toBe('SchemaCheckError');
if (checkResult.schemaCheck.__typename !== 'SchemaCheckError') {
throw new Error('Expected SchemaCheckError');
}
const schemaCheckId = checkResult.schemaCheck.schemaCheck?.id;
expect(schemaCheckId).toBeDefined();
const mutationResult = await execute({
document: graphql(/* GraphQL */ `
mutation ApproveFailedSchemaCheckWithDifferentAuthorFormats(
$input: ApproveFailedSchemaCheckInput!
) {
approveFailedSchemaCheck(input: $input) {
ok {
schemaCheck {
__typename
... on SuccessfulSchemaCheck {
isApproved
approvalComment
cliApprovalMetadata {
displayName
email
}
}
}
}
error {
message
}
}
}
`),
variables: {
input: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
schemaCheckId: schemaCheckId!,
comment: `Testing ${testCase.description}`,
author: testCase.author,
},
},
authToken: writeToken.secret,
}).then(r => r.expectNoGraphQLErrors());
expect(mutationResult.approveFailedSchemaCheck.ok).not.toBeNull();
const approvedCheck = mutationResult.approveFailedSchemaCheck.ok?.schemaCheck;
if (approvedCheck?.__typename === 'SuccessfulSchemaCheck') {
expect(approvedCheck.cliApprovalMetadata?.displayName).toBe(testCase.expected.displayName);
expect(approvedCheck.cliApprovalMetadata?.email).toBe(testCase.expected.email);
}
}
},
);