console/integration-tests/tests/cli/schema.spec.ts

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}`),
);
});
});