mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
1175 lines
37 KiB
TypeScript
1175 lines
37 KiB
TypeScript
/* eslint-disable no-process-env */
|
||
import { createHash, randomUUID } from 'node:crypto';
|
||
import { writeFile } from 'node:fs/promises';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import stripAnsi from 'strip-ansi';
|
||
import { ProjectType, RuleInstanceSeverityLevel } from 'testkit/gql/graphql';
|
||
import * as GraphQLSchema from 'testkit/gql/graphql';
|
||
import type { CompositeSchema } from '@hive/api/__generated__/types';
|
||
import { appCreate, appPublish, createCLI, schemaCheck, schemaPublish } from '../../testkit/cli';
|
||
import { cliOutputSnapshotSerializer } from '../../testkit/cli-snapshot-serializer';
|
||
import { initSeed } from '../../testkit/seed';
|
||
import { createPolicy } from '../api/policy/policy-check.spec';
|
||
|
||
expect.addSnapshotSerializer(cliOutputSnapshotSerializer);
|
||
|
||
describe.each([ProjectType.Stitching, ProjectType.Federation, ProjectType.Single])(
|
||
'%s',
|
||
projectType => {
|
||
const serviceNameArgs = projectType === ProjectType.Single ? [] : ['--service', 'test'];
|
||
const serviceUrlArgs =
|
||
projectType === ProjectType.Single ? [] : ['--url', 'http://localhost:4000'];
|
||
const serviceName = projectType === ProjectType.Single ? undefined : 'test';
|
||
const serviceUrl = projectType === ProjectType.Single ? undefined : 'http://localhost:4000';
|
||
|
||
test.concurrent(
|
||
'can publish a schema with breaking, warning and safe changes',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
...serviceNameArgs,
|
||
...serviceUrlArgs,
|
||
'fixtures/init-schema-detailed.graphql',
|
||
]),
|
||
).resolves.toMatchSnapshot('schemaPublish');
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
...serviceNameArgs,
|
||
'--registry.accessToken',
|
||
secret,
|
||
'fixtures/breaking-schema-detailed.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('schemaCheck');
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'can publish and check a schema with target:registry:read access',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
...serviceNameArgs,
|
||
...serviceUrlArgs,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchSnapshot('schemaPublish');
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--service',
|
||
'test',
|
||
'--registry.accessToken',
|
||
secret,
|
||
'fixtures/nonbreaking-schema.graphql',
|
||
]),
|
||
).resolves.toMatchSnapshot('schemaCheck (non-breaking)');
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
...serviceNameArgs,
|
||
'--registry.accessToken',
|
||
secret,
|
||
'fixtures/breaking-schema.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('schemaCheck (breaking)');
|
||
},
|
||
);
|
||
|
||
test
|
||
.skipIf(projectType === ProjectType.Single)
|
||
.concurrent('publish validates the service name', async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
'--service',
|
||
'900',
|
||
...serviceUrlArgs,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('onlyNumbers');
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
'--service',
|
||
'asdf$#%^#@!#',
|
||
...serviceUrlArgs,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('specialCharacters');
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
'--service',
|
||
'valid-name0',
|
||
...serviceUrlArgs,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchSnapshot('success');
|
||
});
|
||
|
||
test
|
||
.skipIf(projectType === ProjectType.Single)
|
||
.concurrent('check validates the service name', async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
'--service',
|
||
'900',
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('onlyNumbers');
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
'--service',
|
||
'asdf$#%^#@!#',
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('specialCharacters');
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
'--service',
|
||
'valid-name0',
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchSnapshot('success');
|
||
});
|
||
|
||
test.concurrent(
|
||
'publishing invalid schema SDL provides meaningful feedback for the user.',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
...serviceNameArgs,
|
||
...serviceUrlArgs,
|
||
'fixtures/init-invalid-schema.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('schemaPublish');
|
||
},
|
||
);
|
||
|
||
test.concurrent('schema:publish should print a link to the website', async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { organization, inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { project, target, createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
...serviceNameArgs,
|
||
...serviceUrlArgs,
|
||
'--registry.accessToken',
|
||
secret,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatch(
|
||
`Available at ${process.env.HIVE_APP_BASE_URL}/${organization.slug}/${project.slug}/${target.slug}`,
|
||
);
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
...serviceNameArgs,
|
||
...serviceUrlArgs,
|
||
'--registry.accessToken',
|
||
secret,
|
||
'fixtures/nonbreaking-schema.graphql',
|
||
]),
|
||
).resolves.toMatch(
|
||
`Available at ${process.env.HIVE_APP_BASE_URL}/${organization.slug}/${project.slug}/${target.slug}/history/`,
|
||
);
|
||
});
|
||
|
||
test.concurrent(
|
||
'schema:check should notify user when registry is empty',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
secret,
|
||
...serviceNameArgs,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchSnapshot('schemaCheck');
|
||
},
|
||
);
|
||
|
||
test.concurrent('schema:check should throw on corrupted schema', async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
...serviceNameArgs,
|
||
'--registry.accessToken',
|
||
secret,
|
||
'fixtures/missing-type.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('schemaCheck');
|
||
});
|
||
|
||
test.concurrent(
|
||
'schema:publish should see Invalid Token error when token is invalid',
|
||
async ({ expect }) => {
|
||
const invalidToken = createHash('md5').update('nope').digest('hex').substring(0, 31);
|
||
await expect(
|
||
schemaPublish([
|
||
...serviceNameArgs,
|
||
...serviceUrlArgs,
|
||
'--registry.accessToken',
|
||
invalidToken,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).rejects.toMatchSnapshot('schemaPublish');
|
||
},
|
||
);
|
||
|
||
test
|
||
.skipIf(projectType === ProjectType.Single)
|
||
.concurrent(
|
||
'can update the service url and show it in comparison query',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken, compareToPreviousVersion, fetchVersions } =
|
||
await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
const cli = createCLI({
|
||
readonly: secret,
|
||
readwrite: secret,
|
||
});
|
||
|
||
const sdl = /* GraphQL */ `
|
||
type Query {
|
||
users: [User!]
|
||
}
|
||
|
||
type User {
|
||
id: ID!
|
||
name: String!
|
||
email: String!
|
||
}
|
||
`;
|
||
|
||
await expect(
|
||
cli.publish({
|
||
sdl,
|
||
commit: 'push1',
|
||
serviceName,
|
||
serviceUrl,
|
||
expect: 'latest-composable',
|
||
}),
|
||
).resolves.toMatchSnapshot('schemaPublish (initial)');
|
||
|
||
const newServiceUrl = serviceUrl + '/new';
|
||
await expect(
|
||
cli.publish({
|
||
sdl,
|
||
commit: 'push2',
|
||
serviceName,
|
||
serviceUrl: newServiceUrl,
|
||
expect: 'latest-composable',
|
||
}),
|
||
).resolves.toMatchSnapshot('schemaPublish (new)');
|
||
|
||
const versions = await fetchVersions(3);
|
||
expect(versions).toHaveLength(2);
|
||
|
||
const versionWithNewServiceUrl = versions[0];
|
||
|
||
expect(await compareToPreviousVersion(versionWithNewServiceUrl.id)).toEqual(
|
||
expect.objectContaining({
|
||
target: expect.objectContaining({
|
||
schemaVersion: expect.objectContaining({
|
||
safeSchemaChanges: expect.objectContaining({
|
||
nodes: expect.arrayContaining([
|
||
expect.objectContaining({
|
||
criticality: 'Dangerous',
|
||
message: `[${serviceName}] New service url: '${newServiceUrl}' (previously: '${serviceUrl}')`,
|
||
}),
|
||
]),
|
||
}),
|
||
}),
|
||
}),
|
||
}),
|
||
);
|
||
},
|
||
);
|
||
|
||
test
|
||
.skipIf(projectType !== ProjectType.Federation)
|
||
.concurrent(
|
||
'can update a service without providing a url if previously published',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken, compareToPreviousVersion, fetchVersions } =
|
||
await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
const cli = createCLI({
|
||
readonly: secret,
|
||
readwrite: secret,
|
||
});
|
||
|
||
const sdl = /* GraphQL */ `
|
||
type Query {
|
||
users: [User!]
|
||
}
|
||
|
||
type User {
|
||
id: ID!
|
||
name: String!
|
||
email: String!
|
||
}
|
||
`;
|
||
|
||
await expect(
|
||
cli.publish({
|
||
sdl,
|
||
commit: 'push1',
|
||
serviceName,
|
||
serviceUrl,
|
||
expect: 'latest-composable',
|
||
}),
|
||
).resolves.toMatchSnapshot('schema publish initial');
|
||
|
||
const sdl2 = /* GraphQL */ `
|
||
type Query {
|
||
users: [User!]
|
||
}
|
||
|
||
type User {
|
||
id: ID!
|
||
name: String!
|
||
email: String!
|
||
phone: String
|
||
}
|
||
`;
|
||
|
||
await expect(
|
||
cli.publish({
|
||
sdl: sdl2,
|
||
commit: 'push2',
|
||
serviceName,
|
||
serviceUrl: undefined,
|
||
expect: 'latest-composable',
|
||
}),
|
||
).resolves.toMatchSnapshot('schema publish same url');
|
||
|
||
const versions = await fetchVersions(3);
|
||
expect(versions).toHaveLength(2);
|
||
|
||
const versionWithNewServiceUrl = versions[0];
|
||
|
||
const schema = versionWithNewServiceUrl.schemas.nodes?.[0];
|
||
expect(schema.__typename).toBe('CompositeSchema');
|
||
expect((schema as CompositeSchema).url).toBe('http://localhost:4000');
|
||
|
||
expect(await compareToPreviousVersion(versionWithNewServiceUrl.id)).toEqual(
|
||
expect.objectContaining({
|
||
target: expect.objectContaining({
|
||
schemaVersion: expect.objectContaining({
|
||
safeSchemaChanges: {
|
||
nodes: expect.anything(),
|
||
},
|
||
schemaCompositionErrors: null,
|
||
}),
|
||
}),
|
||
}),
|
||
);
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'schema:fetch can fetch a schema with target:registry:read access',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret, latestSchema } = await createTargetAccessToken({});
|
||
|
||
const cli = createCLI({
|
||
readonly: secret,
|
||
readwrite: secret,
|
||
});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--commit',
|
||
'abc123',
|
||
...serviceNameArgs,
|
||
...serviceUrlArgs,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchSnapshot('schemaPublish');
|
||
|
||
const schema = await latestSchema();
|
||
const numSchemas = schema.latestVersion?.schemas.nodes.length;
|
||
const fetchCmd = cli.fetch({
|
||
type: 'subgraphs',
|
||
commit: 'abc123',
|
||
});
|
||
const rHeader = `service\\s+url\\s+date`;
|
||
const rUrl = `http:\\/\\/\\S+(:\\d+)?|n/a`;
|
||
const rSubgraph = `[-]+\\s+\\S+\\s+(${rUrl})\\s+\\S+Z\\s+`;
|
||
const rFooter = `subgraphs length: ${numSchemas}`;
|
||
await expect(fetchCmd).resolves.toMatch(
|
||
new RegExp(`${rHeader}\\s+(${rSubgraph}){${numSchemas}}${rFooter}`),
|
||
);
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'schema:fetch can fetch a latest schema with target:registry:read access',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
const cli = createCLI({
|
||
readonly: secret,
|
||
readwrite: secret,
|
||
});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
...serviceNameArgs,
|
||
...serviceUrlArgs,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchSnapshot('schemaPublish');
|
||
|
||
const fetchCmd = cli.fetch({
|
||
type: 'sdl',
|
||
});
|
||
await expect(fetchCmd).resolves.toMatchSnapshot('latest sdl');
|
||
},
|
||
);
|
||
|
||
test.skipIf(projectType !== ProjectType.Single)(
|
||
'schema:check rejects a `--url` argument in single projects',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaCheck(
|
||
[
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--service',
|
||
'example',
|
||
'--url',
|
||
'https://example.graphql-hive.com/graphql',
|
||
'--author',
|
||
'Kamil',
|
||
'fixtures/init-schema.graphql',
|
||
],
|
||
{
|
||
// set these environment variables to "emulate" a GitHub actions environment
|
||
// We set GITHUB_EVENT_PATH to "" because on our CI it can be present and we want
|
||
// consistent snapshot output behaviour.
|
||
GITHUB_ACTIONS: '1',
|
||
GITHUB_REPOSITORY: 'foo/foo',
|
||
GITHUB_EVENT_PATH: '',
|
||
},
|
||
),
|
||
).rejects.toMatchSnapshot();
|
||
},
|
||
);
|
||
|
||
test.skipIf(projectType === ProjectType.Single)(
|
||
'schema:check accepts a `--url` argument in distributed projects',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(projectType);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaCheck(
|
||
[
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--service',
|
||
'example',
|
||
'--url',
|
||
'https://example.graphql-hive.com/graphql',
|
||
'--author',
|
||
'Kamil',
|
||
'fixtures/init-schema.graphql',
|
||
],
|
||
{
|
||
// set these environment variables to "emulate" a GitHub actions environment
|
||
// We set GITHUB_EVENT_PATH to "" because on our CI it can be present and we want
|
||
// consistent snapshot output behaviour.
|
||
GITHUB_ACTIONS: '1',
|
||
GITHUB_REPOSITORY: 'foo/foo',
|
||
GITHUB_EVENT_PATH: '',
|
||
},
|
||
),
|
||
).resolves.toMatchSnapshot();
|
||
},
|
||
);
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'schema:publish with --target parameter matching the access token (slug)',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject, organization } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken, project, target } = await createProject();
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
const targetSlug = [organization.slug, project.slug, target.slug].join('/');
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--target',
|
||
targetSlug,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI SUCCESS OUTPUT :::::::::::::::::
|
||
|
||
stdout--------------------------------------------:
|
||
✔ Published initial schema.
|
||
ℹ Available at http://__URL__
|
||
`);
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'schema:publish with --target parameter matching the access token (UUID)',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken, target } = await createProject();
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--target',
|
||
target.id,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI SUCCESS OUTPUT :::::::::::::::::
|
||
|
||
stdout--------------------------------------------:
|
||
✔ Published initial schema.
|
||
ℹ Available at http://__URL__
|
||
`);
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'schema:publish fails with --target parameter not matching the access token (slug)',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject();
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
const targetSlug = 'i/do/not-match';
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--target',
|
||
targetSlug,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).rejects.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI FAILURE OUTPUT :::::::::::::::
|
||
exitCode------------------------------------------:
|
||
1
|
||
stderr--------------------------------------------:
|
||
› Error: No access (reason: "Missing permission for performing
|
||
› 'schemaVersion:publish' on resource") (Request ID: __REQUEST_ID__) [115]
|
||
› > See https://__URL__ for
|
||
› a complete list of error codes and recommended fixes.
|
||
› To disable this message set HIVE_NO_ERROR_TIP=1
|
||
› Reference: __ID__
|
||
stdout--------------------------------------------:
|
||
__NONE__
|
||
`);
|
||
},
|
||
);
|
||
|
||
test('schema:check gives correct error message for missing `--service` name flag in federation project', async ({
|
||
expect,
|
||
}) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken } = await createProject(ProjectType.Federation);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaCheck(
|
||
[
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--github',
|
||
'--author',
|
||
'Kamil',
|
||
'fixtures/init-schema.graphql',
|
||
],
|
||
{
|
||
// set these environment variables to "emulate" a GitHub actions environment
|
||
// We set GITHUB_EVENT_PATH to "" because on our CI it can be present and we want
|
||
// consistent snapshot output behaviour.
|
||
GITHUB_ACTIONS: '1',
|
||
GITHUB_REPOSITORY: 'foo/foo',
|
||
GITHUB_EVENT_PATH: '',
|
||
},
|
||
),
|
||
).rejects.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI FAILURE OUTPUT :::::::::::::::
|
||
exitCode------------------------------------------:
|
||
1
|
||
stderr--------------------------------------------:
|
||
› Warning: Could not resolve pull request number. Are you running this
|
||
› command on a 'pull_request' event?
|
||
› See https://__URL__
|
||
› b-workflow-for-ci
|
||
stdout--------------------------------------------:
|
||
✖ Detected 1 error
|
||
|
||
- Missing service name
|
||
`);
|
||
});
|
||
|
||
test('schema:check without `--target` flag fails for organization access token', async ({
|
||
expect,
|
||
}) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { createOrganizationAccessToken } = await createOrg();
|
||
const { privateAccessKey } = await createOrganizationAccessToken({
|
||
permissions: ['schemaCheck:create', 'project:describe'],
|
||
resources: {
|
||
mode: GraphQLSchema.ResourceAssignmentModeType.All,
|
||
},
|
||
});
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
privateAccessKey,
|
||
'--author',
|
||
'Kamil',
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).rejects.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI FAILURE OUTPUT :::::::::::::::
|
||
exitCode------------------------------------------:
|
||
3
|
||
stderr--------------------------------------------:
|
||
› Error: Missing 1 required argument:
|
||
› TARGET The target on which the action is performed. This can either be a
|
||
› slug following the format "$organizationSlug/$projectSlug/$targetSlug"
|
||
› (e.g "the-guild/graphql-hive/staging") or an UUID (e.g.
|
||
› "a0f4c605-6541-4350-8cfe-b31f21a4bf80"). [102]
|
||
› > See https://__URL__ for
|
||
› a complete list of error codes and recommended fixes.
|
||
› To disable this message set HIVE_NO_ERROR_TIP=1
|
||
stdout--------------------------------------------:
|
||
__NONE__
|
||
`);
|
||
});
|
||
|
||
test('schema:check with `--target` flag succeeds for organization access token', async ({
|
||
expect,
|
||
}) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { createOrganizationAccessToken, createProject, organization } = await createOrg();
|
||
const { project, target } = await createProject();
|
||
const { privateAccessKey: privateKey } = await createOrganizationAccessToken({
|
||
permissions: ['schemaCheck:create', 'project:describe'],
|
||
resources: {
|
||
mode: GraphQLSchema.ResourceAssignmentModeType.All,
|
||
},
|
||
});
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
privateKey,
|
||
'--author',
|
||
'Kamil',
|
||
'--target',
|
||
`${organization.slug}/${project.slug}/${target.slug}`,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI SUCCESS OUTPUT :::::::::::::::::
|
||
|
||
stdout--------------------------------------------:
|
||
✔ Schema registry is empty, nothing to compare your schema with.
|
||
View full report:
|
||
http://__URL__
|
||
`);
|
||
});
|
||
|
||
test('schema:publish without `--target` flag fails for organization access token', async ({
|
||
expect,
|
||
}) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { createOrganizationAccessToken } = await createOrg();
|
||
const { privateAccessKey: privateKey } = await createOrganizationAccessToken({
|
||
permissions: ['project:describe', 'schemaVersion:publish'],
|
||
resources: {
|
||
mode: GraphQLSchema.ResourceAssignmentModeType.All,
|
||
},
|
||
});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
privateKey,
|
||
'--author',
|
||
'Kamil',
|
||
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).rejects.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI FAILURE OUTPUT :::::::::::::::
|
||
exitCode------------------------------------------:
|
||
3
|
||
stderr--------------------------------------------:
|
||
› Error: Missing 1 required argument:
|
||
› TARGET The target on which the action is performed. This can either be a
|
||
› slug following the format "$organizationSlug/$projectSlug/$targetSlug"
|
||
› (e.g "the-guild/graphql-hive/staging") or an UUID (e.g.
|
||
› "a0f4c605-6541-4350-8cfe-b31f21a4bf80"). [102]
|
||
› > See https://__URL__ for
|
||
› a complete list of error codes and recommended fixes.
|
||
› To disable this message set HIVE_NO_ERROR_TIP=1
|
||
stdout--------------------------------------------:
|
||
__NONE__
|
||
`);
|
||
});
|
||
|
||
test('schema:publish with `--target` flag succeeds for organization access token', async ({
|
||
expect,
|
||
}) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { createOrganizationAccessToken, organization, createProject } = await createOrg();
|
||
const { project, target } = await createProject();
|
||
const { privateAccessKey: privateKey } = await createOrganizationAccessToken({
|
||
permissions: ['project:describe', 'schemaVersion:publish'],
|
||
resources: {
|
||
mode: GraphQLSchema.ResourceAssignmentModeType.All,
|
||
},
|
||
});
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
privateKey,
|
||
'--author',
|
||
'Kamil',
|
||
'--target',
|
||
`${organization.slug}/${project.slug}/${target.slug}`,
|
||
'fixtures/init-schema.graphql',
|
||
]),
|
||
).resolves.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI SUCCESS OUTPUT :::::::::::::::::
|
||
|
||
stdout--------------------------------------------:
|
||
✔ Published initial schema.
|
||
ℹ Available at http://__URL__
|
||
`);
|
||
});
|
||
|
||
test.concurrent(
|
||
'schema:check --forceSafe auto-approves breaking changes 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({
|
||
mode: 'readWrite',
|
||
});
|
||
|
||
await schemaPublish([
|
||
'--registry.accessToken',
|
||
writeToken.secret,
|
||
'--commit',
|
||
'abc123',
|
||
'fixtures/init-schema.graphql',
|
||
]);
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
writeToken.secret,
|
||
'--commit',
|
||
'def456',
|
||
'--forceSafe',
|
||
'--target',
|
||
`${organization.slug}/${project.slug}/${target.slug}`,
|
||
'fixtures/breaking-schema.graphql',
|
||
]),
|
||
).resolves.toContain('Breaking changes were expected (forced)');
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'schema:check --forceSafe fails when schema policy errors prevent approval',
|
||
async ({ expect }) => {
|
||
const { createOrg } = 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({
|
||
mode: 'readWrite',
|
||
});
|
||
|
||
await schemaPublish([
|
||
'--registry.accessToken',
|
||
writeToken.secret,
|
||
'--commit',
|
||
'abc123',
|
||
'fixtures/init-schema.graphql',
|
||
]);
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
writeToken.secret,
|
||
'--commit',
|
||
'def456',
|
||
'--forceSafe',
|
||
'--target',
|
||
`${organization.slug}/${project.slug}/${target.slug}`,
|
||
'fixtures/breaking-schema.graphql',
|
||
]),
|
||
).rejects.toThrow('Failed to auto-approve: Schema check has schema policy errors');
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'schema:check displays enum deprecation reason with single quotes correctly',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { createProject } = await createOrg();
|
||
const { createTargetAccessToken } = await createProject(ProjectType.Single);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Test',
|
||
'--commit',
|
||
'init',
|
||
'fixtures/enum-with-deprecation-init.graphql',
|
||
]);
|
||
|
||
const result = await schemaCheck([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'fixtures/enum-with-deprecation-change.graphql',
|
||
]);
|
||
|
||
expect(stripAnsi(result)).toContain(
|
||
"Enum value Status.INACTIVE was deprecated with reason Use 'DISABLED' instead, it's clearer",
|
||
);
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'huge schema causing composition to go OOM gives correct error message',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { createProject } = await createOrg();
|
||
const { createTargetAccessToken } = await createProject(ProjectType.Federation);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await expect(
|
||
schemaCheck([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Test',
|
||
'--commit',
|
||
'init',
|
||
'--service foo',
|
||
'--url http://localhost.localhost/graphql',
|
||
'fixtures/huge-schema.graphql',
|
||
]),
|
||
).rejects.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI FAILURE OUTPUT :::::::::::::::
|
||
exitCode------------------------------------------:
|
||
1
|
||
stderr--------------------------------------------:
|
||
__NONE__
|
||
stdout--------------------------------------------:
|
||
✖ Detected 1 error
|
||
|
||
- Composition exceeded resource limits. Please contact the Hive Console Team.
|
||
|
||
View full report:
|
||
http://__URL__
|
||
`);
|
||
},
|
||
);
|
||
|
||
test.concurrent(
|
||
'schema:check displays affected app deployments for breaking changes',
|
||
async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { createProject, setFeatureFlag } = await createOrg();
|
||
await setFeatureFlag('appDeployments', true);
|
||
const { createTargetAccessToken } = await createProject(ProjectType.Single);
|
||
const { secret } = await createTargetAccessToken({});
|
||
|
||
await schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Test',
|
||
'--commit',
|
||
'init',
|
||
'fixtures/init-schema.graphql',
|
||
]);
|
||
|
||
const operationsFile = join(tmpdir(), `operations-${randomUUID()}.json`);
|
||
await writeFile(
|
||
operationsFile,
|
||
JSON.stringify({
|
||
'op-hash-1': 'query GetUserEmails { users { id email } }',
|
||
'op-hash-2': 'query GetUserProfile { users { id name email } }',
|
||
}),
|
||
);
|
||
|
||
await appCreate([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--name',
|
||
'test-app',
|
||
'--version',
|
||
'1.0.0',
|
||
operationsFile,
|
||
]);
|
||
|
||
await appPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--name',
|
||
'test-app',
|
||
'--version',
|
||
'1.0.0',
|
||
]);
|
||
|
||
try {
|
||
await schemaCheck(['--registry.accessToken', secret, 'fixtures/breaking-schema.graphql']);
|
||
expect.fail('Expected schema check to fail with breaking changes');
|
||
} catch (error: any) {
|
||
const output = stripAnsi(error.message || error.stderr || String(error));
|
||
expect(output).toContain('[1 app deployment affected]');
|
||
}
|
||
},
|
||
);
|
||
|
||
test.concurrent('schema:publish ignores SDL formatting', async ({ expect }) => {
|
||
const { createOrg } = await initSeed().createOwner();
|
||
const { inviteAndJoinMember, createProject, organization } = await createOrg();
|
||
await inviteAndJoinMember();
|
||
const { createTargetAccessToken, project, target } = await createProject(ProjectType.Federation);
|
||
const { secret, latestSchema } = await createTargetAccessToken({});
|
||
|
||
const targetSlug = [organization.slug, project.slug, target.slug].join('/');
|
||
|
||
await expect(
|
||
schemaPublish([
|
||
'--registry.accessToken',
|
||
secret,
|
||
'--author',
|
||
'Kamil',
|
||
'--target',
|
||
targetSlug,
|
||
'--service',
|
||
'whitespace',
|
||
'--url',
|
||
'https://example.graphql-hive.com/graphql',
|
||
'fixtures/whitespace-oddity.graphql',
|
||
]),
|
||
).resolves.toMatchInlineSnapshot(`
|
||
:::::::::::::::: CLI SUCCESS OUTPUT :::::::::::::::::
|
||
|
||
stdout--------------------------------------------:
|
||
✔ Published initial schema.
|
||
ℹ Available at http://__URL__
|
||
`);
|
||
|
||
const latest = await latestSchema();
|
||
expect(latest.latestVersion?.schemas.nodes?.[0]?.source).toMatchInlineSnapshot(`
|
||
"""
|
||
Multi line comment:
|
||
1. Foo
|
||
2. Bar
|
||
3. Should stay in a list format
|
||
"""
|
||
type Query {
|
||
status: Status
|
||
}
|
||
|
||
enum Status {
|
||
ACTIVE
|
||
INACTIVE
|
||
PENDING
|
||
}
|
||
`);
|
||
|
||
// API Schema maintains formatting
|
||
expect(latest.latestVersion?.sdl).toEqual(
|
||
expect.stringContaining(`"""
|
||
Multi line comment:
|
||
1. Foo
|
||
2. Bar
|
||
3. Should stay in a list format
|
||
"""`),
|
||
);
|
||
|
||
// Supergraph maintains formatting
|
||
expect(latest.latestVersion?.supergraph).toEqual(
|
||
expect.stringContaining(`"""
|
||
Multi line comment:
|
||
1. Foo
|
||
2. Bar
|
||
3. Should stay in a list format
|
||
"""`),
|
||
);
|
||
});
|