/* eslint-disable no-process-env */ import { createHash } from 'node:crypto'; import { ProjectType } from 'testkit/gql/graphql'; import { createCLI, schemaCheck, 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}')`, }), ]), }), }), }), }), ); }); });