import crypto from 'node:crypto'; import bcrypt from 'bcryptjs'; import { ProjectType } from 'testkit/gql/graphql'; import { ApolloGateway } from '@apollo/gateway'; import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import { DeleteObjectsCommand, GetObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3'; import { createSupergraphManager } from '@graphql-hive/apollo'; import { graphql } from '../../testkit/gql'; import { execute } from '../../testkit/graphql'; import { initSeed } from '../../testkit/seed'; import { getServiceHost, KnownServices } from '../../testkit/utils'; const s3Client = new S3Client({ endpoint: 'http://127.0.0.1:9000', region: 'auto', credentials: { accessKeyId: 'minioadmin', secretAccessKey: 'minioadmin', }, forcePathStyle: true, }); async function deleteS3Object(s3Client: S3Client, bucketName: string, keysToDelete: Array) { if (keysToDelete.length) { const deleteObjectsCommand = new DeleteObjectsCommand({ Bucket: bucketName, Delete: { Objects: keysToDelete.map(key => ({ Key: key })) }, }); await s3Client.send(deleteObjectsCommand); } } async function putS3Object(s3Client: S3Client, bucketName: string, key: string, body: string) { const putObjectCommand = new PutObjectCommand({ Bucket: bucketName, Key: key, Body: body, }); await s3Client.send(putObjectCommand); } async function fetchS3ObjectArtifact( bucketName: string, key: string, ): Promise<{ body: string; eTag: string }> { const getObjectCommand = new GetObjectCommand({ Bucket: bucketName, Key: key, }); const result = await s3Client.send(getObjectCommand); return { body: await result.Body!.transformToString(), eTag: result.ETag!, }; } function buildEndpointUrl( baseUrl: string, targetId: string, resourceType: 'sdl' | 'supergraph' | 'services' | 'metadata', ) { return `${baseUrl}${targetId}/${resourceType}`; } function buildVersionedEndpointUrl( baseUrl: string, targetId: string, versionId: string, resourceType: 'sdl' | 'supergraph' | 'services' | 'metadata', ) { return `${baseUrl}${targetId}/version/${versionId}/${resourceType}`; } function buildVersionedContractEndpointUrl( baseUrl: string, targetId: string, versionId: string, contractName: string, resourceType: 'sdl' | 'supergraph', ) { return `${baseUrl}${targetId}/version/${versionId}/contracts/${contractName}/${resourceType}`; } const CreateContractMutation = graphql(` mutation CreateContractMutationCDN($input: CreateContractInput!) { createContract(input: $input) { ok { createdContract { id } } error { message } } } `); function generateLegacyToken(targetId: string) { const encoder = new TextEncoder(); return ( crypto // eslint-disable-next-line no-process-env .createHmac('sha256', process.env.HIVE_ENCRYPTION_SECRET!) .update(encoder.encode(targetId)) .digest('base64') ); } /** * We have both a CDN that runs as part of the server and one that runs as a standalone service (cloudflare worker). */ function runArtifactsCDNTests( name: string, runtime: { service: KnownServices; port: number; path: string }, ) { const getBaseEndpoint = () => getServiceHost(runtime.service, runtime.port).then(v => `http://${v}${runtime.path}`); describe(`Artifacts CDN ${name}`, () => { test.concurrent( 'legacy cdn access key can be used for accessing artifacts', async ({ expect }) => { const endpointBaseUrl = await getBaseEndpoint(); const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { target, createTargetAccessToken } = await createProject(ProjectType.Single); const token = await createTargetAccessToken({}); await token .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); // manually generate CDN access token for legacy support const legacyToken = generateLegacyToken(target.id); const legacyTokenHash = await bcrypt.hash(legacyToken, await bcrypt.genSalt(10)); await putS3Object(s3Client, 'artifacts', `cdn-legacy-keys/${target.id}`, legacyTokenHash); const url = buildEndpointUrl(endpointBaseUrl, target.id, 'sdl'); const response = await fetch(url, { method: 'GET', headers: { 'x-hive-cdn-key': legacyToken, }, }); expect(response.status).toBe(200); expect(await response.text()).toMatchInlineSnapshot(` type Query { ping: String } `); }, ); test.concurrent( 'legacy deleting cdn access token from s3 revokes artifact cdn access', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, target } = await createProject(ProjectType.Single); const writeToken = await createTargetAccessToken({}); await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); // manually generate CDN access token for legacy support const legacyToken = generateLegacyToken(target.id); const legacyTokenHash = await bcrypt.hash(legacyToken, await bcrypt.genSalt(10)); await putS3Object(s3Client, 'artifacts', `cdn-legacy-keys/${target.id}`, legacyTokenHash); const endpointBaseUrl = await getBaseEndpoint(); // First roundtrip const url = buildEndpointUrl(endpointBaseUrl, target.id, 'sdl'); let response = await fetch(url, { method: 'GET', headers: { 'x-hive-cdn-key': legacyToken, }, }); expect(response.status).toBe(200); expect(await response.text()).toMatchInlineSnapshot(` type Query { ping: String } `); await deleteS3Object(s3Client, 'artifacts', [`cdn-legacy-keys/${target.id}`]); // Second roundtrip response = await fetch(url, { method: 'GET', headers: { 'x-hive-cdn-key': legacyToken, }, }); expect(response.status).toBe(403); }, ); test.concurrent('access without credentials', async () => { const endpointBaseUrl = await getBaseEndpoint(); const url = buildEndpointUrl(endpointBaseUrl, 'i-do-not-exist', 'sdl'); const response = await fetch(url, { method: 'GET' }); expect(response.status).toBe(400); expect(response.headers.get('content-type')).toContain('application/json'); expect(await response.json()).toEqual({ code: 'MISSING_AUTH_KEY', error: 'Hive CDN authentication key is missing', description: 'Please refer to the documentation for more details: https://docs.graphql-hive.com/features/registry-usage ', }); expect(response.headers.get('location')).toBe(null); }); test.concurrent('access invalid credentials', async () => { const endpointBaseUrl = await getBaseEndpoint(); const url = buildEndpointUrl(endpointBaseUrl, 'i-do-not-exist', 'sdl'); const response = await fetch(url, { method: 'GET', headers: { 'x-hive-cdn-key': 'skrrtbrrrt', }, }); expect(response.status).toBe(403); expect(response.headers.get('content-type')).toContain('application/json'); expect(await response.json()).toEqual({ code: 'INVALID_AUTH_KEY', error: 'Hive CDN authentication key is invalid, or it does not match the requested target ID.', description: 'Please refer to the documentation for more details: https://docs.graphql-hive.com/features/registry-usage ', }); expect(response.headers.get('location')).toBe(null); }); test.concurrent('access SDL artifact with valid credentials', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Single, ); const writeToken = await createTargetAccessToken({}); // Publish Schema const publishSchemaResult = await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); const url = buildEndpointUrl(endpointBaseUrl, target.id, 'sdl'); const response = await fetch(url, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, redirect: 'manual', }); expect(response.status).toBe(200); const body = await response.text(); expect(body).toMatchInlineSnapshot(` type Query { ping: String } `); const artifactContents = await fetchS3ObjectArtifact( 'artifacts', `artifact/${target.id}/sdl`, ); expect(artifactContents.body).toMatchInlineSnapshot(` type Query { ping: String } `); }); test.concurrent('access services artifact with valid credentials', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Federation, ); const writeToken = await createTargetAccessToken({}); // Publish Schema const publishSchemaResult = await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, service: 'ping', url: 'http://ping.com', }) .then(r => r.expectNoGraphQLErrors()); expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); // check if artifact exists in bucket const artifactContents = await fetchS3ObjectArtifact( 'artifacts', `artifact/${target.id}/services`, ); expect(artifactContents.body).toMatchInlineSnapshot( '[{"name":"ping","sdl":"type Query { ping: String }","url":"http://ping.com"}]', ); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); const url = buildEndpointUrl(endpointBaseUrl, target.id, 'services'); let response = await fetch(url, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, redirect: 'manual', }); expect(response.status).toBe(200); const body = await response.text(); expect(body).toMatchInlineSnapshot( '[{"name":"ping","sdl":"type Query { ping: String }","url":"http://ping.com"}]', ); }); test.concurrent('access services artifact with if-none-match header', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Federation, ); const writeToken = await createTargetAccessToken({}); // Publish Schema const publishSchemaResult = await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, service: 'ping', url: 'http://ping.com', }) .then(r => r.expectNoGraphQLErrors()); expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); // check if artifact exists in bucket const artifactContents = await fetchS3ObjectArtifact( 'artifacts', `artifact/${target.id}/services`, ); expect(artifactContents.body).toMatchInlineSnapshot( '[{"name":"ping","sdl":"type Query { ping: String }","url":"http://ping.com"}]', ); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); const url = buildEndpointUrl(endpointBaseUrl, target.id, 'services'); const response = await fetch(url, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, 'if-none-match': artifactContents.eTag, }, redirect: 'manual', }); expect(response.status).toBe(304); }); test.concurrent('access services artifact with ApolloGateway and ApolloServer', async () => { const endpointBaseUrl = await getBaseEndpoint(); const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Federation, ); const writeToken = await createTargetAccessToken({}); // Publish Schema const publishSchemaResult = await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: 'type Query { ping: String }', service: 'ping', url: 'http://ping.com', }) .then(r => r.expectNoGraphQLErrors()); expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); const cdnAccessResult = await createCdnAccess(); const gateway = new ApolloGateway({ supergraphSdl: createSupergraphManager({ endpoint: endpointBaseUrl + target.id, key: cdnAccessResult.secretAccessToken, }), }); const server = new ApolloServer({ gateway }); try { const { url } = await startStandaloneServer(server); const response = await fetch(url, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json', }, body: JSON.stringify({ query: ` { __schema { types { name fields { name } } } } `, }), }); expect(response.status).toBe(200); const result = await response.json(); expect(result.data.__schema.types).toContainEqual({ name: 'Query', fields: [{ name: 'ping' }], }); } finally { await server.stop(); } }); test.concurrent('access versioned SDL artifact with valid credentials', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Single, ); const writeToken = await createTargetAccessToken({}); // Publish Schema const publishSchemaResult = await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); // Fetch the latest valid version to get the version ID const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Test latest endpoint const latestUrl = buildEndpointUrl(endpointBaseUrl, target.id, 'sdl'); const latestResponse = await fetch(latestUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(latestResponse.status).toBe(200); const latestBody = await latestResponse.text(); // Test versioned endpoint const versionedUrl = buildVersionedEndpointUrl(endpointBaseUrl, target.id, versionId!, 'sdl'); const versionedResponse = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(versionedResponse.status).toBe(200); const versionedBody = await versionedResponse.text(); // Both should return the same content expect(versionedBody).toBe(latestBody); expect(versionedBody).toMatchInlineSnapshot(` type Query { ping: String } `); // Verify the versioned S3 key exists const versionedArtifact = await fetchS3ObjectArtifact( 'artifacts', `artifact/${target.id}/version/${versionId}/sdl`, ); expect(versionedArtifact.body).toBe(latestBody); expect(versionedResponse.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable', ); }); test.concurrent( 'versioned artifact returns 404 for non-existent version', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Single, ); const writeToken = await createTargetAccessToken({}); // Publish Schema await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Use a non-existent but valid UUID const nonExistentVersionId = '00000000-0000-0000-0000-000000000000'; const versionedUrl = buildVersionedEndpointUrl( endpointBaseUrl, target.id, nonExistentVersionId, 'sdl', ); const response = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(response.status).toBe(404); }, ); test.concurrent( 'versioned artifact returns 404 for invalid UUID format', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Single, ); const writeToken = await createTargetAccessToken({}); // Publish Schema await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Use an invalid UUID format const invalidVersionId = 'not-a-valid-uuid'; const versionedUrl = buildVersionedEndpointUrl( endpointBaseUrl, target.id, invalidVersionId, 'sdl', ); const response = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(response.status).toBe(404); }, ); test.concurrent('access versioned federation supergraph artifact', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Federation, ); const writeToken = await createTargetAccessToken({}); // Publish Schema const publishSchemaResult = await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, service: 'ping', url: 'http://ping.com', }) .then(r => r.expectNoGraphQLErrors()); expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); // Fetch the latest valid version to get the version ID const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Test versioned supergraph endpoint const versionedUrl = buildVersionedEndpointUrl( endpointBaseUrl, target.id, versionId!, 'supergraph', ); const versionedResponse = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(versionedResponse.status).toBe(200); const supergraphBody = await versionedResponse.text(); expect(supergraphBody).toContain('schema'); // Verify the versioned S3 key exists const versionedArtifact = await fetchS3ObjectArtifact( 'artifacts', `artifact/${target.id}/version/${versionId}/supergraph`, ); expect(versionedArtifact.body).toBe(supergraphBody); expect(versionedResponse.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable', ); }); test.concurrent('access versioned federation services artifact', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Federation, ); const writeToken = await createTargetAccessToken({}); // Publish Schema const publishSchemaResult = await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, service: 'ping', url: 'http://ping.com', }) .then(r => r.expectNoGraphQLErrors()); expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); // Fetch the latest valid version to get the version ID const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Test versioned services endpoint const versionedUrl = buildVersionedEndpointUrl( endpointBaseUrl, target.id, versionId!, 'services', ); const versionedResponse = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(versionedResponse.status).toBe(200); expect(versionedResponse.headers.get('content-type')).toContain('application/json'); const servicesBody = await versionedResponse.text(); expect(servicesBody).toMatchInlineSnapshot( '[{"name":"ping","sdl":"type Query { ping: String }","url":"http://ping.com"}]', ); // Verify the versioned S3 key exists const versionedArtifact = await fetchS3ObjectArtifact( 'artifacts', `artifact/${target.id}/version/${versionId}/services`, ); expect(versionedArtifact.body).toBe(servicesBody); expect(versionedResponse.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable', ); }); test.concurrent('access versioned federation metadata artifact', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Federation, ); const writeToken = await createTargetAccessToken({}); // Publish Schema with metadata const publishSchemaResult = await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, service: 'ping', url: 'http://ping.com', metadata: JSON.stringify({ version: '1.0' }), }) .then(r => r.expectNoGraphQLErrors()); expect(publishSchemaResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); // Fetch the latest valid version to get the version ID const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Test versioned metadata endpoint const versionedUrl = buildVersionedEndpointUrl( endpointBaseUrl, target.id, versionId!, 'metadata', ); const versionedResponse = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(versionedResponse.status).toBe(200); expect(versionedResponse.headers.get('content-type')).toContain('application/json'); const metadataBody = await versionedResponse.text(); // Federation metadata contains the metadata we published expect(metadataBody).toContain('version'); // Verify the versioned S3 key exists const versionedArtifact = await fetchS3ObjectArtifact( 'artifacts', `artifact/${target.id}/version/${versionId}/metadata`, ); expect(versionedArtifact.body).toBe(metadataBody); expect(versionedResponse.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable', ); expect(versionedResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); }); test.concurrent('versioned artifact access without credentials', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, target } = await createProject(ProjectType.Single); const writeToken = await createTargetAccessToken({}); await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const endpointBaseUrl = await getBaseEndpoint(); const versionedUrl = buildVersionedEndpointUrl(endpointBaseUrl, target.id, versionId!, 'sdl'); // Request without credentials const response = await fetch(versionedUrl, { method: 'GET' }); expect(response.status).toBe(400); expect(response.headers.get('content-type')).toContain('application/json'); expect(await response.json()).toEqual({ code: 'MISSING_AUTH_KEY', error: 'Hive CDN authentication key is missing', description: 'Please refer to the documentation for more details: https://docs.graphql-hive.com/features/registry-usage ', }); }); test.concurrent('versioned artifact access with invalid credentials', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, target } = await createProject(ProjectType.Single); const writeToken = await createTargetAccessToken({}); await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const endpointBaseUrl = await getBaseEndpoint(); const versionedUrl = buildVersionedEndpointUrl(endpointBaseUrl, target.id, versionId!, 'sdl'); // Request with invalid credentials const response = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': 'invalid-key', }, }); expect(response.status).toBe(403); expect(response.headers.get('content-type')).toContain('application/json'); expect(await response.json()).toEqual({ code: 'INVALID_AUTH_KEY', error: 'Hive CDN authentication key is invalid, or it does not match the requested target ID.', description: 'Please refer to the documentation for more details: https://docs.graphql-hive.com/features/registry-usage ', }); }); test.concurrent('CDN response includes x-hive-schema-version-id header', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Single, ); const writeToken = await createTargetAccessToken({}); // Publish Schema await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); // Fetch the latest valid version to get the version ID const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Test latest endpoint returns x-hive-schema-version-id header const latestUrl = buildEndpointUrl(endpointBaseUrl, target.id, 'sdl'); const latestResponse = await fetch(latestUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(latestResponse.status).toBe(200); expect(latestResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); // Test versioned endpoint also returns x-hive-schema-version-id header const versionedUrl = buildVersionedEndpointUrl(endpointBaseUrl, target.id, versionId!, 'sdl'); const versionedResponse = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(versionedResponse.status).toBe(200); expect(versionedResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); }); test.concurrent('versioned artifact with if-none-match returns 304', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Single, ); const writeToken = await createTargetAccessToken({}); // Publish Schema await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); // Fetch the latest valid version to get the version ID const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // First request to get ETag const versionedUrl = buildVersionedEndpointUrl(endpointBaseUrl, target.id, versionId!, 'sdl'); const firstResponse = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(firstResponse.status).toBe(200); const etag = firstResponse.headers.get('etag'); expect(etag).toBeDefined(); // Second request with If-None-Match should return 304 const secondResponse = await fetch(versionedUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, 'if-none-match': etag!, }, }); expect(secondResponse.status).toBe(304); }); test.concurrent( 'versioned artifact remains immutable after new schema publish', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Single, ); const writeToken = await createTargetAccessToken({}); // Publish V1 schema await writeToken .publishSchema({ author: 'Kamil', commit: 'v1', sdl: `type Query { ping: String }`, }) .then(r => r.expectNoGraphQLErrors()); const v1Version = await writeToken.fetchLatestValidSchema(); const v1VersionId = v1Version.latestValidVersion?.id; expect(v1VersionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Fetch V1 content const v1Url = buildVersionedEndpointUrl(endpointBaseUrl, target.id, v1VersionId!, 'sdl'); const v1Response = await fetch(v1Url, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken }, }); expect(v1Response.status).toBe(200); const v1Content = await v1Response.text(); expect(v1Content).toContain('ping'); // Publish V2 schema with different content await writeToken .publishSchema({ author: 'Kamil', commit: 'v2', sdl: `type Query { ping: String, pong: String }`, }) .then(r => r.expectNoGraphQLErrors()); const v2Version = await writeToken.fetchLatestValidSchema(); const v2VersionId = v2Version.latestValidVersion?.id; expect(v2VersionId).toBeDefined(); expect(v2VersionId).not.toBe(v1VersionId); // Verify V1 versioned endpoint still returns original content const v1ResponseAfterV2 = await fetch(v1Url, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken }, }); expect(v1ResponseAfterV2.status).toBe(200); const v1ContentAfterV2 = await v1ResponseAfterV2.text(); expect(v1ContentAfterV2).toBe(v1Content); expect(v1ContentAfterV2).not.toContain('pong'); // Verify latest endpoint returns V2 content const latestUrl = buildEndpointUrl(endpointBaseUrl, target.id, 'sdl'); const latestResponse = await fetch(latestUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken }, }); expect(latestResponse.status).toBe(200); const latestContent = await latestResponse.text(); expect(latestContent).toContain('pong'); // Verify headers point to correct versions expect(v1ResponseAfterV2.headers.get('x-hive-schema-version-id')).toBe(v1VersionId); expect(latestResponse.headers.get('x-hive-schema-version-id')).toBe(v2VersionId); }, ); test.concurrent('x-hive-schema-version-id header on all artifact types', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target } = await createProject( ProjectType.Federation, ); const writeToken = await createTargetAccessToken({}); // Publish Federation schema with metadata (required for metadata artifact) await writeToken .publishSchema({ author: 'Kamil', commit: 'abc123', sdl: `type Query { ping: String }`, service: 'ping', url: 'http://ping.com', metadata: JSON.stringify({ version: '1.0' }), }) .then(r => r.expectNoGraphQLErrors()); const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Test SDL artifact const sdlResponse = await fetch(buildEndpointUrl(endpointBaseUrl, target.id, 'sdl'), { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken }, }); expect(sdlResponse.status).toBe(200); expect(sdlResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); // Test services artifact const servicesResponse = await fetch( buildEndpointUrl(endpointBaseUrl, target.id, 'services'), { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken }, }, ); expect(servicesResponse.status).toBe(200); expect(servicesResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); // Test supergraph artifact const supergraphResponse = await fetch( buildEndpointUrl(endpointBaseUrl, target.id, 'supergraph'), { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken }, }, ); expect(supergraphResponse.status).toBe(200); expect(supergraphResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); // Test metadata artifact const metadataResponse = await fetch( buildEndpointUrl(endpointBaseUrl, target.id, 'metadata'), { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken }, }, ); expect(metadataResponse.status).toBe(200); expect(metadataResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); }); test.concurrent( 'access versioned contract artifact with valid credentials', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, setFeatureFlag } = await createOrg(); const { createTargetAccessToken, createCdnAccess, target, setNativeFederation } = await createProject(ProjectType.Federation); await setFeatureFlag('compareToPreviousComposableVersion', true); await setNativeFederation(true); const writeToken = await createTargetAccessToken({}); // Publish initial schema with @tag directive await writeToken .publishSchema({ sdl: /* GraphQL */ ` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]) type Query { hello: String helloHidden: String @tag(name: "internal") } `, service: 'hello', url: 'http://hello.com', }) .then(r => r.expectNoGraphQLErrors()); // Create a contract const createContractResult = await execute({ document: CreateContractMutation, variables: { input: { target: { byId: target.id }, contractName: 'my-contract', removeUnreachableTypesFromPublicApiSchema: true, includeTags: ['internal'], }, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(createContractResult.createContract.error).toBeNull(); // Publish schema again to generate contract artifacts await writeToken .publishSchema({ sdl: /* GraphQL */ ` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]) type Query { hello: String helloHidden: String @tag(name: "internal") } `, service: 'hello', url: 'http://hello.com', }) .then(r => r.expectNoGraphQLErrors()); // Fetch the latest valid version to get the version ID const latestVersion = await writeToken.fetchLatestValidSchema(); const versionId = latestVersion.latestValidVersion?.id; expect(versionId).toBeDefined(); const cdnAccessResult = await createCdnAccess(); const endpointBaseUrl = await getBaseEndpoint(); // Test versioned contract SDL endpoint const versionedContractSdlUrl = buildVersionedContractEndpointUrl( endpointBaseUrl, target.id, versionId!, 'my-contract', 'sdl', ); const contractSdlResponse = await fetch(versionedContractSdlUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(contractSdlResponse.status).toBe(200); const contractSdlBody = await contractSdlResponse.text(); expect(contractSdlBody).toContain('helloHidden'); expect(contractSdlResponse.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable', ); expect(contractSdlResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); // Test versioned contract supergraph endpoint const versionedContractSupergraphUrl = buildVersionedContractEndpointUrl( endpointBaseUrl, target.id, versionId!, 'my-contract', 'supergraph', ); const contractSupergraphResponse = await fetch(versionedContractSupergraphUrl, { method: 'GET', headers: { 'x-hive-cdn-key': cdnAccessResult.secretAccessToken, }, }); expect(contractSupergraphResponse.status).toBe(200); expect(contractSupergraphResponse.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable', ); expect(contractSupergraphResponse.headers.get('x-hive-schema-version-id')).toBe(versionId); }, ); }); } runArtifactsCDNTests('API Mirror', { service: 'server', port: 8082, path: '/artifacts/v1/' }); describe('CDN token', () => { const TargetCDNAccessTokensQuery = graphql(` query TargetCDNAccessTokens($selector: TargetSelectorInput!, $after: String, $first: Int = 2) { target(reference: { bySelector: $selector }) { cdnAccessTokens(first: $first, after: $after) { pageInfo { hasNextPage endCursor } edges { cursor node { id firstCharacters lastCharacters createdAt } } } } } `); const DeleteCDNAccessTokenMutation = graphql(` mutation DeleteCDNAccessToken($input: DeleteCdnAccessTokenInput!) { deleteCdnAccessToken(input: $input) { error { message } ok { deletedCdnAccessTokenId } } } `); it('connection pagination', async () => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, target, createCdnAccess, createTargetAccessToken } = await createProject( ProjectType.Federation, ); await Promise.all(new Array(5).fill(0).map(() => createCdnAccess())); let result = await execute({ document: TargetCDNAccessTokensQuery, variables: { selector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(result.target!.cdnAccessTokens.edges).toHaveLength(2); expect(result.target!.cdnAccessTokens.pageInfo.hasNextPage).toBe(true); let endCursor = result.target!.cdnAccessTokens.pageInfo.endCursor; result = await execute({ document: TargetCDNAccessTokensQuery, variables: { selector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, after: endCursor, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(result.target!.cdnAccessTokens.edges).toHaveLength(2); expect(result.target!.cdnAccessTokens.pageInfo.hasNextPage).toBe(true); endCursor = result.target!.cdnAccessTokens.pageInfo.endCursor; result = await execute({ document: TargetCDNAccessTokensQuery, variables: { selector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, after: endCursor, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(result.target!.cdnAccessTokens.edges).toHaveLength(1); expect(result.target!.cdnAccessTokens.pageInfo.hasNextPage).toBe(false); }); it('new created access tokens are added at the beginning of the connection', async () => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, target, createCdnAccess } = await createProject(ProjectType.Federation); await createCdnAccess(); const firstResult = await execute({ document: TargetCDNAccessTokensQuery, variables: { selector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, first: 2, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); const firstId = firstResult.target!.cdnAccessTokens.edges[0].node.id; expect(firstResult.target!.cdnAccessTokens.edges).toHaveLength(1); await createCdnAccess(); const secondResult = await execute({ document: TargetCDNAccessTokensQuery, variables: { selector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, first: 2, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(secondResult.target!.cdnAccessTokens.edges).toHaveLength(2); expect(secondResult.target!.cdnAccessTokens.edges[1].node.id).toBe(firstId); }); it('delete cdn access token', async () => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, target, createCdnAccess } = await createProject(ProjectType.Federation); await createCdnAccess(); let paginatedResult = await execute({ document: TargetCDNAccessTokensQuery, variables: { selector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, first: 1, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(paginatedResult.target!.cdnAccessTokens.edges).toHaveLength(1); const deleteResult = await execute({ document: DeleteCDNAccessTokenMutation, variables: { input: { target: { bySelector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, }, cdnAccessTokenId: paginatedResult.target!.cdnAccessTokens.edges[0].node.id, }, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(deleteResult.deleteCdnAccessToken.ok).toBeDefined(); expect(deleteResult.deleteCdnAccessToken.error).toBeNull(); expect(deleteResult.deleteCdnAccessToken.ok!.deletedCdnAccessTokenId).toBe( paginatedResult.target!.cdnAccessTokens.edges[0].node.id, ); paginatedResult = await execute({ document: TargetCDNAccessTokensQuery, variables: { selector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, first: 1, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(paginatedResult.target!.cdnAccessTokens.edges).toHaveLength(0); }); it('delete cdn access token without access', async () => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { target, project, createCdnAccess } = await createProject(ProjectType.Federation); await createCdnAccess(); const paginatedResult = await execute({ document: TargetCDNAccessTokensQuery, variables: { selector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, first: 1, }, authToken: ownerToken, }).then(r => r.expectNoGraphQLErrors()); expect(paginatedResult.target!.cdnAccessTokens.edges).toHaveLength(1); const { ownerToken: otherOwnerToken } = await initSeed().createOwner(); const deleteResult = await execute({ document: DeleteCDNAccessTokenMutation, variables: { input: { target: { bySelector: { organizationSlug: organization.slug, projectSlug: project.slug, targetSlug: target.slug, }, }, cdnAccessTokenId: paginatedResult.target!.cdnAccessTokens.edges[0].node.id, }, }, authToken: otherOwnerToken, }).then(r => r.expectGraphQLErrors()); expect(deleteResult).toEqual( expect.arrayContaining([ expect.objectContaining({ message: `No access (reason: "Missing permission for performing 'cdnAccessToken:modify' on resource")`, }), ]), ); }); });