mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
/* eslint-disable no-process-env */
|
|
import { createHash } from 'node:crypto';
|
|
import { ProjectType } from 'testkit/gql/graphql';
|
|
import { createCLI, schemaCheck, schemaFetch, schemaPublish } from '../../testkit/cli';
|
|
import { initSeed } from '../../testkit/seed';
|
|
|
|
describe.each`
|
|
projectType | model
|
|
${ProjectType.Single} | ${'modern'}
|
|
${ProjectType.Stitching} | ${'modern'}
|
|
${ProjectType.Federation} | ${'modern'}
|
|
${ProjectType.Single} | ${'legacy'}
|
|
${ProjectType.Stitching} | ${'legacy'}
|
|
${ProjectType.Federation} | ${'legacy'}
|
|
`('$projectType ($model)', ({ projectType, model }) => {
|
|
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 () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { inviteAndJoinMember, createProject } = await createOrg();
|
|
await inviteAndJoinMember();
|
|
const { createTargetAccessToken } = await createProject(projectType, {
|
|
useLegacyRegistryModels: model === 'legacy',
|
|
});
|
|
const { secret } = await createTargetAccessToken({});
|
|
|
|
await schemaPublish([
|
|
'--registry.accessToken',
|
|
secret,
|
|
'--author',
|
|
'Kamil',
|
|
'--commit',
|
|
'abc123',
|
|
...serviceNameArgs,
|
|
...serviceUrlArgs,
|
|
'fixtures/init-schema-detailed.graphql',
|
|
]);
|
|
await expect(
|
|
schemaCheck([
|
|
...serviceNameArgs,
|
|
'--registry.accessToken',
|
|
secret,
|
|
'fixtures/breaking-schema-detailed.graphql',
|
|
]),
|
|
).rejects.toThrowError(/breaking changes:|dangerous changes:|safe changes/i);
|
|
});
|
|
|
|
test.concurrent('can publish and check a schema with target:registry:read access', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { inviteAndJoinMember, createProject } = await createOrg();
|
|
await inviteAndJoinMember();
|
|
const { createTargetAccessToken } = await createProject(projectType, {
|
|
useLegacyRegistryModels: model === 'legacy',
|
|
});
|
|
const { secret } = await createTargetAccessToken({});
|
|
|
|
await schemaPublish([
|
|
'--registry.accessToken',
|
|
secret,
|
|
'--author',
|
|
'Kamil',
|
|
'--commit',
|
|
'abc123',
|
|
...serviceNameArgs,
|
|
...serviceUrlArgs,
|
|
'fixtures/init-schema.graphql',
|
|
]);
|
|
|
|
await schemaCheck([
|
|
'--service',
|
|
'test',
|
|
'--registry.accessToken',
|
|
secret,
|
|
'fixtures/nonbreaking-schema.graphql',
|
|
]);
|
|
|
|
await expect(
|
|
schemaCheck([
|
|
...serviceNameArgs,
|
|
'--registry.accessToken',
|
|
secret,
|
|
'fixtures/breaking-schema.graphql',
|
|
]),
|
|
).rejects.toThrowError(/breaking/i);
|
|
});
|
|
|
|
test.concurrent(
|
|
'publishing invalid schema SDL provides meaningful feedback for the user.',
|
|
async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { inviteAndJoinMember, createProject } = await createOrg();
|
|
await inviteAndJoinMember();
|
|
const { createTargetAccessToken } = await createProject(projectType, {
|
|
useLegacyRegistryModels: model === 'legacy',
|
|
});
|
|
const { secret } = await createTargetAccessToken({});
|
|
|
|
const allocatedError = new Error('Should have thrown.');
|
|
try {
|
|
await schemaPublish([
|
|
'--registry.accessToken',
|
|
secret,
|
|
'--author',
|
|
'Kamil',
|
|
'--commit',
|
|
'abc123',
|
|
...serviceNameArgs,
|
|
...serviceUrlArgs,
|
|
'fixtures/init-invalid-schema.graphql',
|
|
]);
|
|
throw allocatedError;
|
|
} catch (err) {
|
|
if (err === allocatedError) {
|
|
throw err;
|
|
}
|
|
expect(String(err)).toMatch(`The SDL is not valid at line 1, column 1:`);
|
|
expect(String(err)).toMatch(`Syntax Error: Unexpected Name "iliketurtles"`);
|
|
}
|
|
},
|
|
);
|
|
|
|
test.concurrent('schema:publish should print a link to the website', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { organization, inviteAndJoinMember, createProject } = await createOrg();
|
|
await inviteAndJoinMember();
|
|
const { project, target, createTargetAccessToken } = await createProject(projectType, {
|
|
useLegacyRegistryModels: model === 'legacy',
|
|
});
|
|
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 () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { inviteAndJoinMember, createProject } = await createOrg();
|
|
await inviteAndJoinMember();
|
|
const { createTargetAccessToken } = await createProject(projectType, {
|
|
useLegacyRegistryModels: model === 'legacy',
|
|
});
|
|
const { secret } = await createTargetAccessToken({});
|
|
|
|
await expect(
|
|
schemaCheck([
|
|
'--registry.accessToken',
|
|
secret,
|
|
...serviceNameArgs,
|
|
'fixtures/init-schema.graphql',
|
|
]),
|
|
).resolves.toMatch('empty');
|
|
});
|
|
|
|
test.concurrent('schema:check should throw on corrupted schema', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { inviteAndJoinMember, createProject } = await createOrg();
|
|
await inviteAndJoinMember();
|
|
const { createTargetAccessToken } = await createProject(projectType, {
|
|
useLegacyRegistryModels: model === 'legacy',
|
|
});
|
|
const { secret } = await createTargetAccessToken({});
|
|
|
|
const output = schemaCheck([
|
|
...serviceNameArgs,
|
|
'--registry.accessToken',
|
|
secret,
|
|
'fixtures/missing-type.graphql',
|
|
]);
|
|
await expect(output).rejects.toThrowError('Unknown type');
|
|
});
|
|
|
|
test.concurrent(
|
|
'schema:publish should see Invalid Token error when token is invalid',
|
|
async () => {
|
|
const invalidToken = createHash('md5').update('nope').digest('hex').substring(0, 31);
|
|
const output = schemaPublish([
|
|
...serviceNameArgs,
|
|
...serviceUrlArgs,
|
|
'--registry.accessToken',
|
|
invalidToken,
|
|
'fixtures/init-schema.graphql',
|
|
]);
|
|
|
|
await expect(output).rejects.toThrowError('Invalid token provided');
|
|
},
|
|
);
|
|
|
|
test
|
|
.skipIf(projectType === ProjectType.Single)
|
|
.concurrent('can update the service url and show it in comparison query', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { inviteAndJoinMember, createProject } = await createOrg();
|
|
await inviteAndJoinMember();
|
|
const { createTargetAccessToken, compareToPreviousVersion, fetchVersions } =
|
|
await createProject(projectType, {
|
|
useLegacyRegistryModels: model === 'legacy',
|
|
});
|
|
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.toMatch(/published/i);
|
|
|
|
const newServiceUrl = serviceUrl + '/new';
|
|
await expect(
|
|
cli.publish({
|
|
sdl,
|
|
commit: 'push2',
|
|
serviceName,
|
|
serviceUrl: newServiceUrl,
|
|
expect: 'latest-composable',
|
|
}),
|
|
).resolves.toMatch(/New service url/i);
|
|
|
|
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.concurrent('schema:fetch can fetch a schema with target:registry:read access', async () => {
|
|
const { createOrg } = await initSeed().createOwner();
|
|
const { inviteAndJoinMember, createProject } = await createOrg();
|
|
await inviteAndJoinMember();
|
|
const { createTargetAccessToken } = await createProject(projectType, {
|
|
useLegacyRegistryModels: model === 'legacy',
|
|
});
|
|
const { secret, latestSchema } = await createTargetAccessToken({});
|
|
|
|
const cli = createCLI({
|
|
readonly: secret,
|
|
readwrite: secret,
|
|
});
|
|
|
|
await schemaPublish([
|
|
'--registry.accessToken',
|
|
secret,
|
|
'--author',
|
|
'Kamil',
|
|
'--commit',
|
|
'abc123',
|
|
...serviceNameArgs,
|
|
...serviceUrlArgs,
|
|
'fixtures/init-schema.graphql',
|
|
]);
|
|
|
|
const schema = await latestSchema();
|
|
const numSchemas = schema.latestVersion?.schemas.nodes.length;
|
|
const fetchCmd = cli.fetch({
|
|
type: 'subgraphs',
|
|
actionId: '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}`),
|
|
);
|
|
});
|
|
});
|