mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
1478 lines
51 KiB
TypeScript
1478 lines
51 KiB
TypeScript
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<string>) {
|
|
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 {\\n ping: String\\n}","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 {\\n ping: String\\n}","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 {\\n ping: String\\n}","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 {\\n ping: String\\n}","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 } = await createOrg();
|
|
const { createTargetAccessToken, createCdnAccess, target } = await createProject(
|
|
ProjectType.Federation,
|
|
);
|
|
|
|
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")`,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|