console/integration-tests/tests/api/app-deployments.spec.ts

1825 lines
45 KiB
TypeScript

import { buildASTSchema, parse } from 'graphql';
import { createLogger } from 'graphql-yoga';
import { waitFor } from 'testkit/flow';
import { initSeed } from 'testkit/seed';
import { getServiceHost } from 'testkit/utils';
import { createHive } from '@graphql-hive/core';
import { graphql } from '../../testkit/gql';
import { execute } from '../../testkit/graphql';
const CreateAppDeployment = graphql(`
mutation CreateAppDeployment($input: CreateAppDeploymentInput!) {
createAppDeployment(input: $input) {
error {
message
details {
appName
appVersion
}
}
ok {
createdAppDeployment {
id
name
version
status
}
}
}
}
`);
const GetAppDeployment = graphql(`
query GetAppDeployment(
$targetSelector: TargetSelectorInput!
$appDeploymentName: String!
$appDeploymentVersion: String!
) {
target(selector: $targetSelector) {
appDeployment(appName: $appDeploymentName, appVersion: $appDeploymentVersion) {
id
lastUsed
}
}
}
`);
const AddDocumentsToAppDeployment = graphql(`
mutation AddDocumentsToAppDeployment($input: AddDocumentsToAppDeploymentInput!) {
addDocumentsToAppDeployment(input: $input) {
error {
message
details {
index
message
}
}
ok {
appDeployment {
id
name
version
status
}
}
}
}
`);
const ActivateAppDeployment = graphql(`
mutation ActivateAppDeployment($input: ActivateAppDeploymentInput!) {
activateAppDeployment(input: $input) {
error {
message
}
ok {
isSkipped
activatedAppDeployment {
id
name
version
status
}
}
}
}
`);
const RetireAppDeployment = graphql(`
mutation RetireAppDeployment($input: RetireAppDeploymentInput!) {
retireAppDeployment(input: $input) {
error {
message
}
ok {
retiredAppDeployment {
id
name
version
status
}
}
}
}
`);
const GetPaginatedPersistedDocuments = graphql(`
query GetPaginatedPersistedDocuments(
$targetSelector: TargetSelectorInput!
$appDeploymentName: String!
$appDeploymentVersion: String!
$first: Int
$cursor: String
) {
target(selector: $targetSelector) {
appDeployment(appName: $appDeploymentName, appVersion: $appDeploymentVersion) {
id
documents(first: $first, after: $cursor) {
edges {
cursor
node {
hash
body
}
}
pageInfo {
hasNextPage
}
}
}
}
}
`);
test('create app deployment, add operations, publish, access via CDN (happy path)', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, createCdnAccess } = await createProject();
const token = await createTargetAccessToken({});
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
hello: String
}
`,
});
const cdnAccess = await createCdnAccess();
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: 'hash',
body: 'query { hello }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: null,
ok: {
appDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { activateAppDeployment } = await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(activateAppDeployment).toEqual({
error: null,
ok: {
activatedAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'active',
},
isSkipped: false,
},
});
const persistedOperationUrl = `${cdnAccess.cdnUrl}/apps/my-app/1.0.0/hash`;
const response = await fetch(persistedOperationUrl, {
method: 'GET',
headers: {
'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
},
});
const txt = await response.text();
expect(txt).toEqual('query { hello }');
expect(response.status).toBe(200);
});
test('create app deployment with same name and version succeed if deployment is not active', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
let createAppDeployment = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
})
.then(res => res.expectNoGraphQLErrors())
.then(res => res.createAppDeployment);
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
createAppDeployment = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
})
.then(res => res.expectNoGraphQLErrors())
.then(res => res.createAppDeployment);
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
});
test('create app deployment with same name and version does not fail if deployment is active', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
let createAppDeployment = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
})
.then(res => res.expectNoGraphQLErrors())
.then(res => res.createAppDeployment);
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { activateAppDeployment } = await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(activateAppDeployment).toEqual({
error: null,
ok: {
activatedAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'active',
},
isSkipped: false,
},
});
createAppDeployment = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
})
.then(res => res.expectNoGraphQLErrors())
.then(res => res.createAppDeployment);
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'active',
},
},
});
});
test('create app deployment fails if app name is empty', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: '',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: {
details: {
appName: 'Must be at least 1 character long',
appVersion: null,
},
message: 'Invalid input',
},
ok: null,
});
});
test('create app deployment fails if app name exceeds length of 256 characters', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: new Array(257).fill('a').join(''),
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: {
details: {
appName: 'Must be at most 64 characters long',
appVersion: null,
},
message: 'Invalid input',
},
ok: null,
});
});
test('create app deployment fails if app version is empty', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'myapp',
appVersion: '',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: {
details: {
appName: null,
appVersion: 'Must be at least 1 character long',
},
message: 'Invalid input',
},
ok: null,
});
});
test('create app deployment fails if app version exceeds length of 256 characters', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: new Array(257).fill('a').join(''),
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: {
details: {
appName: null,
appVersion: 'Must be at most 64 characters long',
},
message: 'Invalid input',
},
ok: null,
});
});
test('create app deployment fails without feature flag enabled for organization', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: {
details: null,
message:
'This organization has no access to app deployments. Please contact the Hive team for early access.',
},
ok: null,
});
});
test('add documents to app deployment fails if there is no initial schema published', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: 'hash',
body: 'query { hello }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: {
details: null,
message: 'No schema has been published yet',
},
ok: null,
});
});
test('add documents to app deployment fails if document hash is less than 1 character', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
hello: String
}
`,
});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: '',
body: 'query { hello }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: {
details: {
index: 0,
message: 'Hash must be at least 1 characters long',
},
message: 'Invalid input, please check the operations.',
},
ok: null,
});
});
test('add documents to app deployment fails if document hash is longer than 256 characters', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
hello: String
}
`,
});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: new Array(129).fill('a').join(''),
body: 'query { hello }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: {
details: {
index: 0,
message: 'Hash must be at most 128 characters long',
},
message: 'Invalid input, please check the operations.',
},
ok: null,
});
});
test('add documents to app deployment fails if document is not parse-able', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
hello: String
}
`,
});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: 'hash',
body: 'qugu',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: {
details: {
index: 0,
message: 'Syntax Error: Unexpected Name "qugu".',
},
message: 'Failed to parse a GraphQL operation.',
},
ok: null,
});
});
test('add documents to app deployment fails if document does not pass validation against the target schema', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
hello: String
}
`,
});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: 'hash',
body: 'query { hi }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: {
details: {
index: 0,
message: 'Cannot query field "hi" on type "Query".',
},
message: 'The GraphQL operation is not valid against the latest schema version.',
},
ok: null,
});
});
test('add documents to app deployment fails if document contains multiple executable operation definitions', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
hello: String
}
`,
});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment).toEqual({
error: null,
ok: {
createdAppDeployment: {
id: expect.any(String),
name: 'my-app',
version: '1.0.0',
status: 'pending',
},
},
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: 'hash',
body: 'query a { hello } query b { hello }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: {
details: {
index: 0,
message:
'Multiple operation definitions found. Only one executable operation definition is allowed per document.',
},
message: 'Only one executable operation definition is allowed per document.',
},
ok: null,
});
});
test('add documents to app deployment fails if app deployment does not exist', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: 'hash',
body: 'query { hello }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: {
details: null,
message: 'App deployment not found',
},
ok: null,
});
});
test('add documents to app deployment fails without feature flag enabled for organization', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
hello: String
}
`,
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: 'hash',
body: 'query { hello }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment).toEqual({
error: {
details: null,
message:
'This organization has no access to app deployments. Please contact the Hive team for early access.',
},
ok: null,
});
});
test('activate app deployment fails if app deployment does not exist', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken } = await createProject();
const token = await createTargetAccessToken({});
const { activateAppDeployment } = await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(activateAppDeployment).toEqual({
error: {
message: 'App deployment not found',
},
ok: null,
});
});
test('activate app deployment succeeds if app deployment is already active', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, target } = await createProject();
const token = await createTargetAccessToken({});
await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
let activateResult = await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(activateResult).toEqual({
activateAppDeployment: {
error: null,
ok: {
activatedAppDeployment: {
id: expect.any(String),
name: 'my-app',
status: 'active',
version: '1.0.0',
},
isSkipped: false,
},
},
});
activateResult = await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(activateResult).toEqual({
activateAppDeployment: {
error: null,
ok: {
activatedAppDeployment: {
id: expect.any(String),
name: 'my-app',
status: 'active',
version: '1.0.0',
},
isSkipped: true,
},
},
});
});
test('activate app deployment fails if app deployment is retired', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, target } = await createProject();
const token = await createTargetAccessToken({});
await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
let activateResult = await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(activateResult).toEqual({
activateAppDeployment: {
error: null,
ok: {
activatedAppDeployment: {
id: expect.any(String),
name: 'my-app',
status: 'active',
version: '1.0.0',
},
isSkipped: false,
},
},
});
const retireResult = await execute({
document: RetireAppDeployment,
variables: {
input: {
targetId: target.id,
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(retireResult).toEqual({
retireAppDeployment: {
error: null,
ok: {
retiredAppDeployment: {
id: expect.any(String),
name: 'my-app',
status: 'active',
version: '1.0.0',
},
},
},
});
activateResult = await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(activateResult).toEqual({
activateAppDeployment: {
error: {
message: 'App deployment is retired',
},
ok: null,
},
});
});
test('retire app deployment fails if app deployment does not exist', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, target } = await createProject();
const token = await createTargetAccessToken({});
const { retireAppDeployment } = await execute({
document: RetireAppDeployment,
variables: {
input: {
targetId: target.id,
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(retireAppDeployment).toEqual({
error: {
message: 'App deployment not found',
},
ok: null,
});
});
test('retire app deployment fails if app deployment is pending (not active)', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, target } = await createProject();
const token = await createTargetAccessToken({});
await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
const { retireAppDeployment } = await execute({
document: RetireAppDeployment,
variables: {
input: {
targetId: target.id,
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(retireAppDeployment).toEqual({
error: {
message: 'App deployment is not active',
},
ok: null,
});
});
test('retire app deployment succeeds if app deployment is active', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, target } = await createProject();
const token = await createTargetAccessToken({});
await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
const { retireAppDeployment } = await execute({
document: RetireAppDeployment,
variables: {
input: {
targetId: target.id,
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(retireAppDeployment).toEqual({
error: null,
ok: {
retiredAppDeployment: {
id: expect.any(String),
name: 'my-app',
status: 'active',
version: '1.0.0',
},
},
});
});
test('retire app deployments makes the persisted operations unavailable via CDN', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject, setFeatureFlag } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, createCdnAccess, target } = await createProject();
const token = await createTargetAccessToken({});
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
hello: String
}
`,
});
const cdnAccess = await createCdnAccess();
await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
documents: [
{
hash: 'hash',
body: 'query { hello }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
const persistedOperationUrl = `${cdnAccess.cdnUrl}/apps/my-app/1.0.0/hash`;
let response = await fetch(persistedOperationUrl, {
method: 'GET',
headers: {
'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
},
});
expect(response.status).toBe(200);
await execute({
document: RetireAppDeployment,
variables: {
input: {
targetId: target.id,
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
response = await fetch(persistedOperationUrl, {
method: 'GET',
headers: {
'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
},
});
expect(response.status).toBe(404);
});
test('retire app deployments fails without feature flag enabled for organization', async () => {
const { createOrg } = await initSeed().createOwner();
const { createProject } = await createOrg();
const { createTargetAccessToken, target } = await createProject();
const token = await createTargetAccessToken({});
const { retireAppDeployment } = await execute({
document: RetireAppDeployment,
variables: {
input: {
targetId: target.id,
appName: 'my-app',
appVersion: '1.0.0',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(retireAppDeployment).toEqual({
error: {
message:
'This organization has no access to app deployments. Please contact the Hive team for early access.',
},
ok: null,
});
});
test('get app deployment documents via GraphQL API', async () => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, setFeatureFlag, organization } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, project, target } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: 'app-version',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment.error).toBeNull();
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
a: String
b: String
c: String
d: String
}
`,
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: 'app-version',
documents: [
{
hash: 'aaa',
body: 'query { a }',
},
{
hash: 'bbb',
body: 'query { b }',
},
{
hash: 'ccc',
body: 'query { c }',
},
{
hash: 'ddd',
body: 'query { d }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment.error).toBeNull();
const result = await execute({
document: GetPaginatedPersistedDocuments,
variables: {
targetSelector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
appDeploymentName: 'app-name',
appDeploymentVersion: 'app-version',
},
authToken: ownerToken,
}).then(res => res.expectNoGraphQLErrors());
expect(result.target).toMatchObject({
appDeployment: {
documents: {
edges: [
{
cursor: 'YWFh',
node: {
body: 'query { a }',
hash: 'aaa',
},
},
{
cursor: 'YmJi',
node: {
body: 'query { b }',
hash: 'bbb',
},
},
{
cursor: 'Y2Nj',
node: {
body: 'query { c }',
hash: 'ccc',
},
},
{
cursor: 'ZGRk',
node: {
body: 'query { d }',
hash: 'ddd',
},
},
],
pageInfo: {
hasNextPage: false,
},
},
id: expect.any(String),
},
});
});
test('paginate app deployment documents via GraphQL API', async () => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, setFeatureFlag, organization } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, project, target } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: 'app-version',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment.error).toBeNull();
await token.publishSchema({
sdl: /* GraphQL */ `
type Query {
a: String
b: String
c: String
d: String
}
`,
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: 'app-version',
documents: [
{
hash: 'aaa',
body: 'query { a }',
},
{
hash: 'bbb',
body: 'query { b }',
},
{
hash: 'ccc',
body: 'query { c }',
},
{
hash: 'ddd',
body: 'query { d }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment.error).toBeNull();
let result = await execute({
document: GetPaginatedPersistedDocuments,
variables: {
targetSelector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
appDeploymentName: 'app-name',
appDeploymentVersion: 'app-version',
first: 1,
},
authToken: ownerToken,
}).then(res => res.expectNoGraphQLErrors());
expect(result.target).toMatchObject({
appDeployment: {
documents: {
edges: [
{
cursor: 'YWFh',
node: {
body: 'query { a }',
hash: 'aaa',
},
},
],
pageInfo: {
hasNextPage: true,
},
},
id: expect.any(String),
},
});
result = await execute({
document: GetPaginatedPersistedDocuments,
variables: {
targetSelector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
appDeploymentName: 'app-name',
appDeploymentVersion: 'app-version',
first: 1,
cursor: 'YWFh',
},
authToken: ownerToken,
}).then(res => res.expectNoGraphQLErrors());
expect(result.target).toMatchObject({
appDeployment: {
documents: {
edges: [
{
cursor: 'YmJi',
node: {
body: 'query { b }',
hash: 'bbb',
},
},
],
pageInfo: {
hasNextPage: true,
},
},
id: expect.any(String),
},
});
result = await execute({
document: GetPaginatedPersistedDocuments,
variables: {
targetSelector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
appDeploymentName: 'app-name',
appDeploymentVersion: 'app-version',
cursor: 'YmJi',
},
authToken: ownerToken,
}).then(res => res.expectNoGraphQLErrors());
expect(result.target).toMatchObject({
appDeployment: {
documents: {
edges: [
{
cursor: 'Y2Nj',
node: {
body: 'query { c }',
hash: 'ccc',
},
},
{
cursor: 'ZGRk',
node: {
body: 'query { d }',
hash: 'ddd',
},
},
],
pageInfo: {
hasNextPage: false,
},
},
id: expect.any(String),
},
});
});
test('app deployment usage reporting', async () => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, setFeatureFlag, organization } = await createOrg();
await setFeatureFlag('appDeployments', true);
const { createTargetAccessToken, project, target } = await createProject();
const token = await createTargetAccessToken({});
const { createAppDeployment } = await execute({
document: CreateAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: 'app-version',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(createAppDeployment.error).toBeNull();
const sdl = /* GraphQL */ `
type Query {
a: String
b: String
c: String
d: String
}
`;
await token.publishSchema({
sdl,
});
const { addDocumentsToAppDeployment } = await execute({
document: AddDocumentsToAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: 'app-version',
documents: [
{
hash: 'aaa',
body: 'query { a }',
},
],
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(addDocumentsToAppDeployment.error).toBeNull();
const { activateAppDeployment } = await execute({
document: ActivateAppDeployment,
variables: {
input: {
appName: 'app-name',
appVersion: 'app-version',
},
},
authToken: token.secret,
}).then(res => res.expectNoGraphQLErrors());
expect(activateAppDeployment.error).toEqual(null);
let data = await execute({
document: GetAppDeployment,
variables: {
targetSelector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
appDeploymentName: 'app-name',
appDeploymentVersion: 'app-version',
},
authToken: ownerToken,
}).then(res => res.expectNoGraphQLErrors());
expect(data.target?.appDeployment?.lastUsed).toEqual(null);
const usageAddress = await getServiceHost('usage', 8081);
const client = createHive({
enabled: true,
token: token.secret,
usage: true,
debug: false,
agent: {
logger: createLogger('debug'),
maxSize: 1,
},
selfHosting: {
usageEndpoint: 'http://' + usageAddress,
graphqlEndpoint: 'http://noop/',
applicationUrl: 'http://noop/',
},
});
const request = new Request('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'x-graphql-client-name': 'app-name',
'x-graphql-client-version': 'app-version',
},
});
await client.collectUsage()(
{
document: parse(`query { a }`),
schema: buildASTSchema(parse(sdl)),
contextValue: { request },
},
{},
'app-name~app-version~aaa',
);
await waitFor(5000);
data = await execute({
document: GetAppDeployment,
variables: {
targetSelector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
appDeploymentName: 'app-name',
appDeploymentVersion: 'app-version',
},
authToken: ownerToken,
}).then(res => res.expectNoGraphQLErrors());
expect(data.target?.appDeployment?.lastUsed).toEqual(expect.any(String));
});