import { TargetAccessScope, ProjectType } from '@app/gql/graphql'; import { createHash } from 'node:crypto'; import { schemaPublish, schemaCheck } from '../../testkit/cli'; import { authenticate } from '../../testkit/auth'; import { createOrganization, joinOrganization, createProject, createToken, fetchSupergraphFromCDN, inviteToOrganization, } from '../../testkit/flow'; test('can publish and check a schema with target:registry:read access', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const invitationResult = await inviteToOrganization( { email: 'some@email.com', organization: org.cleanId, }, owner_access_token ); const inviteCode = invitationResult.body.data?.inviteToOrganizationByEmail.ok?.code; expect(inviteCode).toBeDefined(); // Join const { access_token: member_access_token } = await authenticate('extra'); await joinOrganization(inviteCode!, member_access_token); const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; await schemaPublish([ '--token', writeToken, '--author', 'Kamil', '--commit', 'abc123', 'fixtures/init-schema.graphql', ]); await schemaCheck(['--token', writeToken, 'fixtures/nonbreaking-schema.graphql']); await expect(schemaCheck(['--token', writeToken, 'fixtures/breaking-schema.graphql'])).rejects.toThrowError( /breaking/ ); }); test('publishing a breaking change results in invalid state', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const invitationResult = await inviteToOrganization( { email: 'some@email.com', organization: org.cleanId, }, owner_access_token ); const inviteCode = invitationResult.body.data?.inviteToOrganizationByEmail.ok?.code; expect(inviteCode).toBeDefined(); // Join const { access_token: member_access_token } = await authenticate('extra'); await joinOrganization(inviteCode!, member_access_token); const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; await schemaPublish([ '--token', writeToken, '--author', 'Kamil', '--commit', 'abc123', 'fixtures/init-schema.graphql', ]); await expect(schemaPublish(['--token', writeToken, 'fixtures/breaking-schema.graphql'])).rejects.toThrowError( /breaking/ ); }); test('publishing invalid schema SDL provides meaningful feedback for the user.', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const invitationResult = await inviteToOrganization( { email: 'some@email.com', organization: org.cleanId, }, owner_access_token ); const code = invitationResult.body.data?.inviteToOrganizationByEmail.ok?.code; expect(code).toBeDefined(); // Join const { access_token: member_access_token } = await authenticate('extra'); await joinOrganization(code!, member_access_token); const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; const allocatedError = new Error('Should have thrown.'); try { await schemaPublish([ '--token', writeToken, '--author', 'Kamil', '--commit', 'abc123', '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('service url should be available in supergraph', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const invitationResult = await inviteToOrganization( { email: 'some@email.com', organization: org.cleanId, }, owner_access_token ); const code = invitationResult.body.data?.inviteToOrganizationByEmail.ok?.code; expect(code).toBeDefined(); // Join const { access_token: member_access_token } = await authenticate('extra'); await joinOrganization(code!, member_access_token); const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Federation, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; await schemaPublish([ '--token', writeToken, '--author', 'Kamil', '--commit', 'abc123', '--service', 'users', '--url', 'https://api.com/users-subgraph', 'fixtures/federation-init.graphql', ]); const supergraph = await fetchSupergraphFromCDN( { organization: org.cleanId, project: project.cleanId, target: target.cleanId, }, writeToken ); expect(supergraph.body).toMatch('(name: "users" url: "https://api.com/users-subgraph")'); }); test('service url should be required in Federation', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const invitationResult = await inviteToOrganization( { email: 'some@email.com', organization: org.cleanId, }, owner_access_token ); const code = invitationResult.body.data?.inviteToOrganizationByEmail.ok?.code; expect(code).toBeDefined(); // Join const { access_token: member_access_token } = await authenticate('extra'); await joinOrganization(code!, member_access_token); const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Federation, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; await expect( schemaPublish([ '--token', writeToken, '--author', 'Kamil', '--commit', 'abc123', '--service', 'users', 'fixtures/federation-init.graphql', ]) ).rejects.toThrowError(/url/); }); test('schema:publish should print a link to the website', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'bar', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const invitationResult = await inviteToOrganization( { email: 'some@email.com', organization: org.cleanId, }, owner_access_token ); const code = invitationResult.body.data?.inviteToOrganizationByEmail.ok?.code; expect(code).toBeDefined(); // Join const { access_token: member_access_token } = await authenticate('extra'); await joinOrganization(code!, member_access_token); const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const targets = projectResult.body.data!.createProject.ok!.createdTargets; const target = targets.find(t => t.name === 'development')!; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; await expect(schemaPublish(['--token', writeToken, 'fixtures/init-schema.graphql'])).resolves.toMatch( 'Available at https://app.graphql-hive.com/bar/foo/development' ); await expect(schemaPublish(['--token', writeToken, 'fixtures/nonbreaking-schema.graphql'])).resolves.toMatch( 'Available at https://app.graphql-hive.com/bar/foo/development/history/' ); }); test('schema:check should notify user when registry is empty', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const invitationResult = await inviteToOrganization( { email: 'some@email.com', organization: org.cleanId, }, owner_access_token ); const code = invitationResult.body.data?.inviteToOrganizationByEmail.ok?.code; expect(code).toBeDefined(); // Join const { access_token: member_access_token } = await authenticate('extra'); await joinOrganization(code!, member_access_token); const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; await expect(schemaCheck(['--token', writeToken, 'fixtures/init-schema.graphql'])).resolves.toMatch('empty'); }); test('schema:check should throw on corrupted schema', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const invitationResult = await inviteToOrganization( { email: 'some@email.com', organization: org.cleanId, }, owner_access_token ); const code = invitationResult.body.data?.inviteToOrganizationByEmail.ok?.code; expect(code).toBeDefined(); // Join const { access_token: member_access_token } = await authenticate('extra'); await joinOrganization(code!, member_access_token); const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const target = projectResult.body.data!.createProject.ok!.createdTargets[0]; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; const output = schemaCheck(['--token', writeToken, 'fixtures/missing-type.graphql']); await expect(output).rejects.toThrowError('Unknown type'); }); test('schema:publish should see Invalid Token error when token is invalid', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'foo', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const token = createHash('md5').update('nope').digest('hex').substring(0, 31); const output = schemaPublish(['--token', token, 'fixtures/init-schema.graphql']); await expect(output).rejects.toThrowError('Invalid token provided'); }); test('schema:publish should support experimental_acceptBreakingChanges flag', async () => { const { access_token: owner_access_token } = await authenticate('main'); const orgResult = await createOrganization( { name: 'bar', }, owner_access_token ); const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; const projectResult = await createProject( { organization: org.cleanId, type: ProjectType.Single, name: 'foo', }, owner_access_token ); const project = projectResult.body.data!.createProject.ok!.createdProject; const targets = projectResult.body.data!.createProject.ok!.createdTargets; const target = targets.find(t => t.name === 'development')!; // Create a token with write rights const writeTokenResult = await createToken( { name: 'test', organization: org.cleanId, project: project.cleanId, target: target.cleanId, organizationScopes: [], projectScopes: [], targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], }, owner_access_token ); expect(writeTokenResult.body.errors).not.toBeDefined(); const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; await expect(schemaPublish(['--token', writeToken, 'fixtures/init-schema.graphql'])).resolves.toMatch( 'Available at https://app.graphql-hive.com/bar/foo/development' ); await expect( schemaPublish(['--token', writeToken, '--experimental_acceptBreakingChanges', 'fixtures/breaking-schema.graphql']) ).resolves.toMatch('Available at https://app.graphql-hive.com/bar/foo/development/history/'); });