diff --git a/.vscode/terminals.json b/.vscode/terminals.json index 7f5256d9a..7dbbc38f2 100644 --- a/.vscode/terminals.json +++ b/.vscode/terminals.json @@ -92,6 +92,20 @@ "open": true, "cwd": "packages/services/usage-ingestor", "command": "pnpm dev" + }, + { + "name": "external-composition:dev", + "description": "Run External Composition example", + "open": true, + "cwd": "packages/libraries/external-composition/", + "command": "pnpm example" + }, + { + "name": "external-composition-fed2:dev", + "description": "Run External Composition for Federation 2", + "open": true, + "cwd": "packages/services/external-composition/federation-2", + "command": "pnpm dev" } ] } diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index b02a6f6c2..27bf63418 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -74,7 +74,7 @@ services: clickhouse: image: clickhouse/clickhouse-server:24.4-alpine - mem_limit: 300m + mem_limit: 500m environment: CLICKHOUSE_USER: test CLICKHOUSE_PASSWORD: test diff --git a/docs/TESTING.md b/docs/TESTING.md index 36384bdac..be159b8c9 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -16,41 +16,31 @@ We are using Vitest to test the following concerns: Integration tests are based pre-built Docker images, so you can run it in 2 modes: -#### Running from Source Code +#### Running from built artifact (Docker) -**TL;DR**: Use `pnpm integration:prepare` command to set up the complete environment from locally -running integration tests. You can ignore the rest of the commands in this section, if this script -worked for you, and just run `pnpm test:integration` to run the actual tests. +**To run integration tests locally with a built artifact, you need to build a Docker image for the +services. ** -To run integration tests locally, from the local source code, you need to build a valid Docker -image. +Use `pnpm integration:prepare` command to set up the complete environment from locally running +integration tests. -To do so, follow these instructions: +And then, to run the tests, use the following: -1. Install all deps: `pnpm i` -2. Generate types: `pnpm graphql:generate` -3. Build source code: `pnpm build` -4. Set env vars: - ```bash - export COMMIT_SHA="local" - export RELEASE="local" - export BRANCH_NAME="local" - export BUILD_TYPE="" - export DOCKER_TAG=":local" - ``` -5. Compile a local Docker image by running: - `docker buildx bake -f docker/docker.hcl integration-tests --load` -6. Use Docker Compose to run the built containers (based on `community` compose file), along with - the extra containers: +``` +pnpm test:integration +``` - ```bash - export DOCKER_TAG=":local" - export DOCKER_REGISTRY="" - - docker compose -f ./docker/docker-compose.community.yml -f ./integration-tests/docker-compose.integration.yaml --env-file ./integration-tests/.env up -d --wait - ``` +> Make sure to run the prepare command every time you change your code. -7. Run the tests: `pnpm --filter integration-tests test:integration` +#### Running from source code (local services) + +You can also run integration tests against the local-running services, during development. + +To do so, use the following instructions: + +1. Run Hive from VSCode (using `Start Hive` button). +2. Make sure all services are running correctly. +3. Run integration tests with `pnpm dev:integration` command (from root directory). #### Running from Pre-Built Docker Image diff --git a/integration-tests/local-dev.ts b/integration-tests/local-dev.ts new file mode 100644 index 000000000..962b88d5f --- /dev/null +++ b/integration-tests/local-dev.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'fs'; +import { parse } from 'dotenv'; + +function applyEnv(env: Record) { + for (const key in env) { + process.env[key] = env[key]; + } +} + +const serverEnvVars = parse(readFileSync('../packages/services/server/.env', 'utf-8')); + +applyEnv({ + SUPERTOKENS_CONNECTION_URI: serverEnvVars.SUPERTOKENS_CONNECTION_URI, + SUPERTOKENS_API_KEY: serverEnvVars.SUPERTOKENS_API_KEY, + POSTGRES_USER: serverEnvVars.POSTGRES_USER, + POSTGRES_PASSWORD: serverEnvVars.POSTGRES_PASSWORD, + POSTGRES_DB: serverEnvVars.POSTGRES_DB, + POSTGRES_PORT: serverEnvVars.POSTGRES_PORT, + POSTGRES_HOST: serverEnvVars.POSTGRES_HOST, + HIVE_APP_BASE_URL: serverEnvVars.WEB_APP_URL, + EXTERNAL_COMPOSITION_SECRET: 'secretsecret', + CLICKHOUSE_USER: serverEnvVars.CLICKHOUSE_USERNAME, + CLICKHOUSE_PASSWORD: serverEnvVars.CLICKHOUSE_PASSWORD, + HIVE_ENCRYPTION_SECRET: serverEnvVars.HIVE_ENCRYPTION_SECRET, +}); diff --git a/integration-tests/package.json b/integration-tests/package.json index 62a2eeba2..336c10ab3 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -4,6 +4,7 @@ "type": "module", "private": true, "scripts": { + "dev:integration": "RUN_AGAINST_LOCAL_SERVICES=1 vitest", "prepare:env": "cd ../ && pnpm build:libraries && pnpm build:services", "test:integration": "vitest", "typecheck": "tsc --noEmit" diff --git a/integration-tests/testkit/external-composition.ts b/integration-tests/testkit/external-composition.ts index dc959a2b6..bc99cd205 100644 --- a/integration-tests/testkit/external-composition.ts +++ b/integration-tests/testkit/external-composition.ts @@ -1,10 +1,7 @@ import { getServiceHost } from './utils'; -export const serviceName = 'external_composition'; -export const servicePort = 3012; - export async function history(): Promise { - const dockerAddress = await getServiceHost(serviceName, servicePort); + const dockerAddress = await getServiceHost('external_composition', 3012); const res = await fetch(`http://${dockerAddress}/_history`, { method: 'GET', headers: { diff --git a/integration-tests/testkit/utils.ts b/integration-tests/testkit/utils.ts index caddb75ab..8761bfa78 100644 --- a/integration-tests/testkit/utils.ts +++ b/integration-tests/testkit/utils.ts @@ -11,7 +11,44 @@ function getDockerConnection() { return docker; } -export async function getServiceHost(serviceName: string, servicePort: number): Promise { +const LOCAL_SERVICES = { + server: 3001, + clickhouse: 8123, + emails: 6260, + composition_federation_2: 3069, + usage: 4001, + schema: 6500, + external_composition: 3012, +} as const; + +export type KnownServices = keyof typeof LOCAL_SERVICES; + +export function getCDNPort() { + if (process.env.RUN_AGAINST_LOCAL_SERVICES === '1') { + return 9000; + } + + return 8083; +} + +export function getAppBaseUrl() { + if (process.env.RUN_AGAINST_LOCAL_SERVICES === '1') { + return 'localhost:3000'; + } + + return 'localhost:8080'; +} + +export async function getServiceHost( + serviceName: KnownServices, + servicePort: number, + localhost = true, +): Promise { + if (process.env.RUN_AGAINST_LOCAL_SERVICES === '1') { + return `localhost:${LOCAL_SERVICES[serviceName]}`; + } + + const actualHost = localhost ? 'localhost' : serviceName; const docker = getDockerConnection(); const containers = await docker.listContainers({ filters: JSON.stringify({ @@ -42,7 +79,7 @@ export async function getServiceHost(serviceName: string, servicePort: number): } if (publicPort) { - return `localhost:${publicPort.PublicPort}`; + return `${actualHost}:${publicPort.PublicPort}`; } if (privatePort) { @@ -50,10 +87,10 @@ export async function getServiceHost(serviceName: string, servicePort: number): `Container "${container.Id}" (service: "${serviceName}") expose port "${servicePort}" as "${privatePort.PublicPort}", please consider to update your setup!`, ); - return `localhost:${privatePort.PublicPort}`; + return `${actualHost}:${privatePort.PublicPort}`; } - return `localhost:${servicePort}`; + return `${actualHost}:${servicePort}`; } export function generateUnique() { diff --git a/integration-tests/tests/api/artifacts-cdn.spec.ts b/integration-tests/tests/api/artifacts-cdn.spec.ts index ad8587d86..bbac5735c 100644 --- a/integration-tests/tests/api/artifacts-cdn.spec.ts +++ b/integration-tests/tests/api/artifacts-cdn.spec.ts @@ -14,7 +14,7 @@ import { createSupergraphManager } from '@graphql-hive/apollo'; import { graphql } from '../../testkit/gql'; import { execute } from '../../testkit/graphql'; import { initSeed } from '../../testkit/seed'; -import { getServiceHost } from '../../testkit/utils'; +import { getCDNPort, getServiceHost, KnownServices } from '../../testkit/utils'; const s3Client = new S3Client({ endpoint: 'http://127.0.0.1:9000', @@ -85,7 +85,7 @@ function generateLegacyToken(targetId: string) { */ function runArtifactsCDNTests( name: string, - runtime: { service: string; port: number; path: string }, + runtime: { service: KnownServices; port: number; path: string }, ) { const getBaseEndpoint = () => getServiceHost(runtime.service, runtime.port).then(v => `http://${v}${runtime.path}`); @@ -313,7 +313,7 @@ function runArtifactsCDNTests( const locationUrl = new URL(locationHeader!); expect(locationUrl.protocol).toBe('http:'); expect(locationUrl.hostname).toBe('localhost'); - expect(locationUrl.port).toBe('8083'); + expect(locationUrl.port).toBe(getCDNPort().toString()); response = await fetch(locationHeader!, { method: 'GET', diff --git a/integration-tests/tests/api/oidc-integrations/crud.spec.ts b/integration-tests/tests/api/oidc-integrations/crud.spec.ts index c0f8f367a..90f08fdd4 100644 --- a/integration-tests/tests/api/oidc-integrations/crud.spec.ts +++ b/integration-tests/tests/api/oidc-integrations/crud.spec.ts @@ -44,7 +44,7 @@ const CreateOIDCIntegrationMutation = graphql(` describe('create', () => { describe('permissions="organization:integrations"', () => { - test.concurrent('success', async () => { + test.concurrent('success', async ({ expect }) => { const { ownerToken, createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); @@ -315,7 +315,7 @@ describe('create', () => { `); }); - test.concurrent('error: multiple integrations per organization', async () => { + test.concurrent('error: multiple integrations per organization', async ({ expect }) => { const { ownerToken, createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); @@ -399,7 +399,7 @@ const DeleteOIDCIntegrationMutation = graphql(` describe('delete', () => { describe('permissions="organization:integrations"', () => { - test.concurrent('success', async () => { + test.concurrent('success', async ({ expect }) => { const { ownerToken, createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); @@ -621,7 +621,7 @@ const UpdateOIDCIntegrationMutation = graphql(` describe('update', () => { describe('permissions="organization:integrations"', () => { - test.concurrent('success', async () => { + test.concurrent('success', async ({ expect }) => { const { ownerToken, createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); diff --git a/integration-tests/tests/api/organization/crud.spec.ts b/integration-tests/tests/api/organization/crud.spec.ts index 773c5e8f9..1ce58eadb 100644 --- a/integration-tests/tests/api/organization/crud.spec.ts +++ b/integration-tests/tests/api/organization/crud.spec.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto'; import { changeOrganizationSlug, renameOrganization } from '../../../testkit/flow'; import { initSeed } from '../../../testkit/seed'; -test.concurrent('renaming an organization should NOT affect its cleanId', async () => { +test.concurrent('renaming an organization should NOT affect its cleanId', async ({ expect }) => { const { ownerToken, createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); @@ -22,28 +22,31 @@ test.concurrent('renaming an organization should NOT affect its cleanId', async expect(result?.selector.organization).toEqual(organization.cleanId); }); -test.concurrent('renaming an organization to the same name should be possible', async () => { - const { ownerToken, createOrg } = await initSeed().createOwner(); - const { organization } = await createOrg(); +test.concurrent( + 'renaming an organization to the same name should be possible', + async ({ expect }) => { + const { ownerToken, createOrg } = await initSeed().createOwner(); + const { organization } = await createOrg(); - const renamedOrganizationResult = await renameOrganization( - { - organization: organization.cleanId, - name: organization.name, - }, - ownerToken, - ).then(r => r.expectNoGraphQLErrors()); + const renamedOrganizationResult = await renameOrganization( + { + organization: organization.cleanId, + name: organization.name, + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); - const result = renamedOrganizationResult.updateOrganizationName.ok?.updatedOrganizationPayload; - expect(renamedOrganizationResult.updateOrganizationName.error).toBeNull(); - expect(result?.organization.name).toBe(organization.name); - expect(result?.organization.cleanId).toEqual(organization.cleanId); - expect(result?.selector.organization).toEqual(organization.cleanId); -}); + const result = renamedOrganizationResult.updateOrganizationName.ok?.updatedOrganizationPayload; + expect(renamedOrganizationResult.updateOrganizationName.error).toBeNull(); + expect(result?.organization.name).toBe(organization.name); + expect(result?.organization.cleanId).toEqual(organization.cleanId); + expect(result?.selector.organization).toEqual(organization.cleanId); + }, +); test.concurrent( 'modifying a clean id of an organization to the same value should not change the clean id', - async () => { + async ({ expect }) => { const { ownerToken, createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); @@ -64,29 +67,33 @@ test.concurrent( }, ); -test.concurrent('modifying a clean id of an organization should be possible', async () => { - const { ownerToken, createOrg } = await initSeed().createOwner(); - const { organization } = await createOrg(); +test.concurrent( + 'modifying a clean id of an organization should be possible', + async ({ expect }) => { + const { ownerToken, createOrg } = await initSeed().createOwner(); + const { organization } = await createOrg(); - const newCleanId = randomUUID(); - const changeOrganizationSlugResult = await changeOrganizationSlug( - { - organization: organization.cleanId, - slug: newCleanId, - }, - ownerToken, - ).then(r => r.expectNoGraphQLErrors()); + const newCleanId = randomUUID(); + const changeOrganizationSlugResult = await changeOrganizationSlug( + { + organization: organization.cleanId, + slug: newCleanId, + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); - const result = changeOrganizationSlugResult.updateOrganizationSlug.ok?.updatedOrganizationPayload; - expect(changeOrganizationSlugResult.updateOrganizationSlug.error).toBeNull(); - expect(result?.organization.name).toBe(organization.name); - expect(result?.organization.cleanId).toEqual(newCleanId); - expect(result?.selector.organization).toEqual(newCleanId); -}); + const result = + changeOrganizationSlugResult.updateOrganizationSlug.ok?.updatedOrganizationPayload; + expect(changeOrganizationSlugResult.updateOrganizationSlug.error).toBeNull(); + expect(result?.organization.name).toBe(organization.name); + expect(result?.organization.cleanId).toEqual(newCleanId); + expect(result?.selector.organization).toEqual(newCleanId); + }, +); test.concurrent( 'modifying a clean id of an organization to a reserved keyword should fail', - async () => { + async ({ expect }) => { const { ownerToken, createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); @@ -106,7 +113,7 @@ test.concurrent( test.concurrent( 'modifying a clean id of an organization to a taken clean id should fail', - async () => { + async ({ expect }) => { const { ownerToken, createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); const { organization: anotherOrganization } = await createOrg(); diff --git a/integration-tests/tests/api/organization/get-started.spec.ts b/integration-tests/tests/api/organization/get-started.spec.ts index db0d7c908..4d91d8218 100644 --- a/integration-tests/tests/api/organization/get-started.spec.ts +++ b/integration-tests/tests/api/organization/get-started.spec.ts @@ -7,111 +7,117 @@ import { import { waitFor } from '../../../testkit/flow'; import { initSeed } from '../../../testkit/seed'; -test.concurrent('freshly created organization has Get Started progress at 0%', async () => { - const { createOrg } = await initSeed().createOwner(); - const { fetchOrganizationInfo } = await createOrg(); - const { getStarted: steps1 } = await fetchOrganizationInfo(); +test.concurrent( + 'freshly created organization has Get Started progress at 0%', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { fetchOrganizationInfo } = await createOrg(); + const { getStarted: steps1 } = await fetchOrganizationInfo(); - expect(steps1?.creatingProject).toBe(false); - expect(steps1?.publishingSchema).toBe(false); - expect(steps1?.checkingSchema).toBe(false); - expect(steps1?.invitingMembers).toBe(false); - expect(steps1?.reportingOperations).toBe(false); - expect(steps1?.enablingUsageBasedBreakingChanges).toBe(false); -}); + expect(steps1?.creatingProject).toBe(false); + expect(steps1?.publishingSchema).toBe(false); + expect(steps1?.checkingSchema).toBe(false); + expect(steps1?.invitingMembers).toBe(false); + expect(steps1?.reportingOperations).toBe(false); + expect(steps1?.enablingUsageBasedBreakingChanges).toBe(false); + }, +); -test.concurrent('completing each step should result in updated Get Started progress', async () => { - const { createOrg } = await initSeed().createOwner(); - const { inviteAndJoinMember, fetchOrganizationInfo, createProject } = await createOrg(); - const { target, project, createToken } = await createProject(ProjectType.Single); +test.concurrent( + 'completing each step should result in updated Get Started progress', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { inviteAndJoinMember, fetchOrganizationInfo, createProject } = await createOrg(); + const { target, project, createToken } = await createProject(ProjectType.Single); - const { getStarted: steps } = await fetchOrganizationInfo(); - expect(steps?.creatingProject).toBe(true); // modified - expect(steps?.publishingSchema).toBe(false); - expect(steps?.checkingSchema).toBe(false); - expect(steps?.invitingMembers).toBe(false); - expect(steps?.reportingOperations).toBe(false); - expect(steps?.enablingUsageBasedBreakingChanges).toBe(false); + const { getStarted: steps } = await fetchOrganizationInfo(); + expect(steps?.creatingProject).toBe(true); // modified + expect(steps?.publishingSchema).toBe(false); + expect(steps?.checkingSchema).toBe(false); + expect(steps?.invitingMembers).toBe(false); + expect(steps?.reportingOperations).toBe(false); + expect(steps?.enablingUsageBasedBreakingChanges).toBe(false); - if (!target || !project) { - throw new Error('Failed to create project'); - } + if (!target || !project) { + throw new Error('Failed to create project'); + } - const { - publishSchema, - checkSchema, - collectLegacyOperations: collectOperations, - toggleTargetValidation, - } = await createToken({ - targetScopes: [ - TargetAccessScope.Read, - TargetAccessScope.RegistryRead, - TargetAccessScope.RegistryWrite, - TargetAccessScope.Settings, - ], - projectScopes: [ProjectAccessScope.Read], - organizationScopes: [OrganizationAccessScope.Read], - }); + const { + publishSchema, + checkSchema, + collectLegacyOperations: collectOperations, + toggleTargetValidation, + } = await createToken({ + targetScopes: [ + TargetAccessScope.Read, + TargetAccessScope.RegistryRead, + TargetAccessScope.RegistryWrite, + TargetAccessScope.Settings, + ], + projectScopes: [ProjectAccessScope.Read], + organizationScopes: [OrganizationAccessScope.Read], + }); - // Step: publish schema - await publishSchema({ sdl: 'type Query { foo: String }' }).then(r => r.expectNoGraphQLErrors()); - const { getStarted: steps2 } = await fetchOrganizationInfo(); - expect(steps2?.creatingProject).toBe(true); - expect(steps2?.publishingSchema).toBe(true); // modified - expect(steps2?.checkingSchema).toBe(false); - expect(steps2?.invitingMembers).toBe(false); - expect(steps2?.reportingOperations).toBe(false); - expect(steps2?.enablingUsageBasedBreakingChanges).toBe(false); + // Step: publish schema + await publishSchema({ sdl: 'type Query { foo: String }' }).then(r => r.expectNoGraphQLErrors()); + const { getStarted: steps2 } = await fetchOrganizationInfo(); + expect(steps2?.creatingProject).toBe(true); + expect(steps2?.publishingSchema).toBe(true); // modified + expect(steps2?.checkingSchema).toBe(false); + expect(steps2?.invitingMembers).toBe(false); + expect(steps2?.reportingOperations).toBe(false); + expect(steps2?.enablingUsageBasedBreakingChanges).toBe(false); - // Step: checking schema - await checkSchema('type Query { foo: String bar: String }'); - const { getStarted: steps3 } = await fetchOrganizationInfo(); - expect(steps3?.creatingProject).toBe(true); - expect(steps3?.publishingSchema).toBe(true); - expect(steps3?.checkingSchema).toBe(true); // modified - expect(steps3?.invitingMembers).toBe(false); - expect(steps3?.reportingOperations).toBe(false); - expect(steps3?.enablingUsageBasedBreakingChanges).toBe(false); + // Step: checking schema + await checkSchema('type Query { foo: String bar: String }'); + const { getStarted: steps3 } = await fetchOrganizationInfo(); + expect(steps3?.creatingProject).toBe(true); + expect(steps3?.publishingSchema).toBe(true); + expect(steps3?.checkingSchema).toBe(true); // modified + expect(steps3?.invitingMembers).toBe(false); + expect(steps3?.reportingOperations).toBe(false); + expect(steps3?.enablingUsageBasedBreakingChanges).toBe(false); - // Step: inviting members - await inviteAndJoinMember(); - const { getStarted: steps4 } = await fetchOrganizationInfo(); - expect(steps4?.creatingProject).toBe(true); - expect(steps4?.publishingSchema).toBe(true); - expect(steps4?.checkingSchema).toBe(true); - expect(steps4?.invitingMembers).toBe(true); // modified - expect(steps4?.reportingOperations).toBe(false); - expect(steps4?.enablingUsageBasedBreakingChanges).toBe(false); + // Step: inviting members + await inviteAndJoinMember(); + const { getStarted: steps4 } = await fetchOrganizationInfo(); + expect(steps4?.creatingProject).toBe(true); + expect(steps4?.publishingSchema).toBe(true); + expect(steps4?.checkingSchema).toBe(true); + expect(steps4?.invitingMembers).toBe(true); // modified + expect(steps4?.reportingOperations).toBe(false); + expect(steps4?.enablingUsageBasedBreakingChanges).toBe(false); - // Step: reporting operations - await collectOperations([ - { - operationName: 'foo', - operation: 'query foo { foo }', - fields: ['Query', 'Query.foo'], - execution: { - duration: 2_000_000, - ok: true, - errorsTotal: 0, + // Step: reporting operations + await collectOperations([ + { + operationName: 'foo', + operation: 'query foo { foo }', + fields: ['Query', 'Query.foo'], + execution: { + duration: 2_000_000, + ok: true, + errorsTotal: 0, + }, }, - }, - ]); - await waitFor(5000); - const { getStarted: steps5 } = await fetchOrganizationInfo(); - expect(steps5?.creatingProject).toBe(true); - expect(steps5?.publishingSchema).toBe(true); - expect(steps5?.checkingSchema).toBe(true); - expect(steps5?.invitingMembers).toBe(true); - expect(steps5?.reportingOperations).toBe(true); // modified - expect(steps5?.enablingUsageBasedBreakingChanges).toBe(false); + ]); + await waitFor(8000); + const { getStarted: steps5 } = await fetchOrganizationInfo(); + expect(steps5?.creatingProject).toBe(true); + expect(steps5?.publishingSchema).toBe(true); + expect(steps5?.checkingSchema).toBe(true); + expect(steps5?.invitingMembers).toBe(true); + expect(steps5?.reportingOperations).toBe(true); // modified + expect(steps5?.enablingUsageBasedBreakingChanges).toBe(false); - // Step: target validation - await toggleTargetValidation(true); - const { getStarted: steps6 } = await fetchOrganizationInfo(); - expect(steps6?.creatingProject).toBe(true); - expect(steps6?.publishingSchema).toBe(true); - expect(steps6?.checkingSchema).toBe(true); - expect(steps6?.invitingMembers).toBe(true); - expect(steps6?.reportingOperations).toBe(true); - expect(steps6?.enablingUsageBasedBreakingChanges).toBe(true); // modified -}); + // Step: target validation + await toggleTargetValidation(true); + const { getStarted: steps6 } = await fetchOrganizationInfo(); + expect(steps6?.creatingProject).toBe(true); + expect(steps6?.publishingSchema).toBe(true); + expect(steps6?.checkingSchema).toBe(true); + expect(steps6?.invitingMembers).toBe(true); + expect(steps6?.reportingOperations).toBe(true); + expect(steps6?.enablingUsageBasedBreakingChanges).toBe(true); // modified + }, +); diff --git a/integration-tests/tests/api/organization/members.spec.ts b/integration-tests/tests/api/organization/members.spec.ts index e69071c7f..dc0ac2605 100644 --- a/integration-tests/tests/api/organization/members.spec.ts +++ b/integration-tests/tests/api/organization/members.spec.ts @@ -6,7 +6,7 @@ import { import { history } from '../../../testkit/emails'; import { initSeed } from '../../../testkit/seed'; -test.concurrent('owner of an organization should have all scopes', async () => { +test.concurrent('owner of an organization should have all scopes', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization } = await createOrg(); @@ -23,7 +23,7 @@ test.concurrent('owner of an organization should have all scopes', async () => { }); }); -test.concurrent('invited member should have basic scopes (Viewer role)', async () => { +test.concurrent('invited member should have basic scopes (Viewer role)', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { inviteAndJoinMember } = await createOrg(); const { member } = await inviteAndJoinMember(); @@ -46,7 +46,7 @@ test.concurrent('invited member should have basic scopes (Viewer role)', async ( test.concurrent( 'cannot create a role with an access scope that user has no access to', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { inviteAndJoinMember, organization } = await createOrg(); const { createMemberRole, assignMemberRole, member } = await inviteAndJoinMember(); @@ -80,7 +80,7 @@ test.concurrent( test.concurrent( 'cannot grant an access scope to another user if user has no access to that scope', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { inviteAndJoinMember, organization } = await createOrg(); const { createMemberRole, assignMemberRole, member } = await inviteAndJoinMember(); @@ -120,7 +120,7 @@ test.concurrent( test.concurrent( 'granting no scopes is equal to setting read-only for org, project and target', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { inviteAndJoinMember } = await createOrg(); const { createMemberRole } = await inviteAndJoinMember(); @@ -139,7 +139,7 @@ test.concurrent( }, ); -test.concurrent('cannot downgrade a member when assigning a new role', async () => { +test.concurrent('cannot downgrade a member when assigning a new role', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { inviteAndJoinMember } = await createOrg(); const { createMemberRole, assignMemberRole, member } = await inviteAndJoinMember(); @@ -196,7 +196,7 @@ test.concurrent('cannot downgrade a member when assigning a new role', async () }); }); -test.concurrent('cannot downgrade a member when modifying a role', async () => { +test.concurrent('cannot downgrade a member when modifying a role', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { inviteAndJoinMember } = await createOrg(); const { createMemberRole, assignMemberRole, member, updateMemberRole } = @@ -252,7 +252,7 @@ test.concurrent('cannot downgrade a member when modifying a role', async () => { }); }); -test.concurrent('cannot delete a role with members', async () => { +test.concurrent('cannot delete a role with members', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { inviteAndJoinMember } = await createOrg(); const { createMemberRole, deleteMemberRole, assignMemberRole, member } = @@ -290,7 +290,7 @@ test.concurrent('cannot delete a role with members', async () => { ).rejects.toThrowError('Cannot delete a role with members'); }); -test.concurrent('cannot invite a member with more access than the inviter', async () => { +test.concurrent('cannot invite a member with more access than the inviter', async ({ expect }) => { const seed = initSeed(); const { createOrg } = await seed.createOwner(); const { inviteMember, inviteAndJoinMember, organization } = await createOrg(); @@ -334,7 +334,7 @@ test.concurrent('cannot invite a member with more access than the inviter', asyn expect(inviteCode).toBeDefined(); }); -test.concurrent('email invitation', async () => { +test.concurrent('email invitation', async ({ expect }) => { const seed = initSeed(); const { createOrg } = await seed.createOwner(); const { inviteMember } = await createOrg(); @@ -348,33 +348,36 @@ test.concurrent('email invitation', async () => { expect(sentEmails).toContainEqual(expect.objectContaining({ to: inviteEmail })); }); -test.concurrent('cannot join organization twice using the same invitation code', async () => { - const seed = initSeed(); - const { createOrg } = await seed.createOwner(); - const { inviteMember, joinMemberUsingCode } = await createOrg(); +test.concurrent( + 'cannot join organization twice using the same invitation code', + async ({ expect }) => { + const seed = initSeed(); + const { createOrg } = await seed.createOwner(); + const { inviteMember, joinMemberUsingCode } = await createOrg(); - // Invite - const invitationResult = await inviteMember(); - const inviteCode = invitationResult.ok!.code; - expect(inviteCode).toBeDefined(); + // Invite + const invitationResult = await inviteMember(); + const inviteCode = invitationResult.ok!.code; + expect(inviteCode).toBeDefined(); - // Join - const extra = seed.generateEmail(); - const { access_token: member_access_token } = await seed.authenticate(extra); - const joinResult = await ( - await joinMemberUsingCode(inviteCode, member_access_token) - ).expectNoGraphQLErrors(); + // Join + const extra = seed.generateEmail(); + const { access_token: member_access_token } = await seed.authenticate(extra); + const joinResult = await ( + await joinMemberUsingCode(inviteCode, member_access_token) + ).expectNoGraphQLErrors(); - expect(joinResult.joinOrganization.__typename).toBe('OrganizationPayload'); + expect(joinResult.joinOrganization.__typename).toBe('OrganizationPayload'); - if (joinResult.joinOrganization.__typename !== 'OrganizationPayload') { - throw new Error('Join failed'); - } + if (joinResult.joinOrganization.__typename !== 'OrganizationPayload') { + throw new Error('Join failed'); + } - const other = seed.generateEmail(); - const { access_token: other_access_token } = await seed.authenticate(other); - const otherJoinResult = await ( - await joinMemberUsingCode(inviteCode, other_access_token) - ).expectNoGraphQLErrors(); - expect(otherJoinResult.joinOrganization.__typename).toBe('OrganizationInvitationError'); -}); + const other = seed.generateEmail(); + const { access_token: other_access_token } = await seed.authenticate(other); + const otherJoinResult = await ( + await joinMemberUsingCode(inviteCode, other_access_token) + ).expectNoGraphQLErrors(); + expect(otherJoinResult.joinOrganization.__typename).toBe('OrganizationInvitationError'); + }, +); diff --git a/integration-tests/tests/api/organization/transfer.spec.ts b/integration-tests/tests/api/organization/transfer.spec.ts index b68c392d6..2b0688231 100644 --- a/integration-tests/tests/api/organization/transfer.spec.ts +++ b/integration-tests/tests/api/organization/transfer.spec.ts @@ -12,7 +12,7 @@ import { initSeed } from '../../../testkit/seed'; test.concurrent( 'accessing non-existing ownership transfer request should result in null', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization } = await createOrg(); @@ -28,43 +28,49 @@ test.concurrent( }, ); -test.concurrent('owner should be able to request the ownership transfer to a member', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, inviteAndJoinMember } = await createOrg(); - const { member, memberEmail } = await inviteAndJoinMember(); +test.concurrent( + 'owner should be able to request the ownership transfer to a member', + async ({ expect }) => { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { organization, inviteAndJoinMember } = await createOrg(); + const { member, memberEmail } = await inviteAndJoinMember(); - const transferRequestResult = await requestOrganizationTransfer( - { - organization: organization.cleanId, - user: member.user.id, - }, - ownerToken, - ).then(r => r.expectNoGraphQLErrors()); + const transferRequestResult = await requestOrganizationTransfer( + { + organization: organization.cleanId, + user: member.user.id, + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); - expect(transferRequestResult.requestOrganizationTransfer.ok?.email).toBe(memberEmail); -}); + expect(transferRequestResult.requestOrganizationTransfer.ok?.email).toBe(memberEmail); + }, +); -test.concurrent('non-owner should not be able to request the ownership transfer', async () => { - const { createOrg, ownerEmail } = await initSeed().createOwner(); - const { organization, inviteAndJoinMember, members } = await createOrg(); - const { memberToken } = await inviteAndJoinMember(); - const orgMembers = await members(); +test.concurrent( + 'non-owner should not be able to request the ownership transfer', + async ({ expect }) => { + const { createOrg, ownerEmail } = await initSeed().createOwner(); + const { organization, inviteAndJoinMember, members } = await createOrg(); + const { memberToken } = await inviteAndJoinMember(); + const orgMembers = await members(); - const errors = await requestOrganizationTransfer( - { - organization: organization.cleanId, - user: orgMembers.find(u => u.user.email === ownerEmail)!.user.id, - }, - memberToken, - ).then(r => r.expectGraphQLErrors()); + const errors = await requestOrganizationTransfer( + { + organization: organization.cleanId, + user: orgMembers.find(u => u.user.email === ownerEmail)!.user.id, + }, + memberToken, + ).then(r => r.expectGraphQLErrors()); - expect(errors).toBeDefined(); - expect(errors.length).toBe(1); -}); + expect(errors).toBeDefined(); + expect(errors.length).toBe(1); + }, +); test.concurrent( 'owner should not be able to request the ownership transfer to non-member', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization, inviteAndJoinMember } = await createOrg(); const { memberToken, member } = await inviteAndJoinMember(); @@ -81,71 +87,77 @@ test.concurrent( }, ); -test.concurrent('non-member should not be able to access the transfer request', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, inviteAndJoinMember } = await createOrg(); - const { member } = await inviteAndJoinMember(); +test.concurrent( + 'non-member should not be able to access the transfer request', + async ({ expect }) => { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { organization, inviteAndJoinMember } = await createOrg(); + const { member } = await inviteAndJoinMember(); - const requestTransferResult = await requestOrganizationTransfer( - { - organization: organization.cleanId, - user: member.user.id, - }, - ownerToken, - ).then(r => r.expectNoGraphQLErrors()); + const requestTransferResult = await requestOrganizationTransfer( + { + organization: organization.cleanId, + user: member.user.id, + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); - const code = requestTransferResult.requestOrganizationTransfer.ok?.code; + const code = requestTransferResult.requestOrganizationTransfer.ok?.code; - if (!code) { - throw new Error('Could not create transfer request'); - } + if (!code) { + throw new Error('Could not create transfer request'); + } - const { ownerToken: nonMemberToken } = await initSeed().createOwner(); + const { ownerToken: nonMemberToken } = await initSeed().createOwner(); - const errors = await getOrganizationTransferRequest( - { - organization: organization.cleanId, - code, - }, - nonMemberToken, - ).then(r => r.expectGraphQLErrors()); + const errors = await getOrganizationTransferRequest( + { + organization: organization.cleanId, + code, + }, + nonMemberToken, + ).then(r => r.expectGraphQLErrors()); - expect(errors).toBeDefined(); - expect(errors.length).toBe(1); -}); + expect(errors).toBeDefined(); + expect(errors.length).toBe(1); + }, +); -test.concurrent('non-recipient should not be able to access the transfer request', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, inviteAndJoinMember } = await createOrg(); - const { member } = await inviteAndJoinMember(); - const { memberToken: lonelyMemberToken } = await inviteAndJoinMember(); +test.concurrent( + 'non-recipient should not be able to access the transfer request', + async ({ expect }) => { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { organization, inviteAndJoinMember } = await createOrg(); + const { member } = await inviteAndJoinMember(); + const { memberToken: lonelyMemberToken } = await inviteAndJoinMember(); - const requestTransferResult = await requestOrganizationTransfer( - { - organization: organization.cleanId, - user: member.user.id, - }, - ownerToken, - ).then(r => r.expectNoGraphQLErrors()); + const requestTransferResult = await requestOrganizationTransfer( + { + organization: organization.cleanId, + user: member.user.id, + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); - const code = requestTransferResult.requestOrganizationTransfer.ok?.code; + const code = requestTransferResult.requestOrganizationTransfer.ok?.code; - if (!code) { - throw new Error('Could not create transfer request'); - } + if (!code) { + throw new Error('Could not create transfer request'); + } - const requestResult = await getOrganizationTransferRequest( - { - organization: organization.cleanId, - code, - }, - lonelyMemberToken, - ).then(r => r.expectNoGraphQLErrors()); + const requestResult = await getOrganizationTransferRequest( + { + organization: organization.cleanId, + code, + }, + lonelyMemberToken, + ).then(r => r.expectNoGraphQLErrors()); - expect(requestResult.organizationTransferRequest).toBeNull(); -}); + expect(requestResult.organizationTransferRequest).toBeNull(); + }, +); -test.concurrent('recipient should be able to access the transfer request', async () => { +test.concurrent('recipient should be able to access the transfer request', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, inviteAndJoinMember } = await createOrg(); const { member, memberToken } = await inviteAndJoinMember(); @@ -174,7 +186,7 @@ test.concurrent('recipient should be able to access the transfer request', async expect(requestResult.organizationTransferRequest).not.toBeNull(); }); -test.concurrent('recipient should be able to answer the ownership transfer', async () => { +test.concurrent('recipient should be able to answer the ownership transfer', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, inviteAndJoinMember } = await createOrg(); const { member, memberToken } = await inviteAndJoinMember(); @@ -205,39 +217,42 @@ test.concurrent('recipient should be able to answer the ownership transfer', asy expect(answerResult.answerOrganizationTransferRequest.ok?.accepted).toBe(true); }); -test.concurrent('non-member should not be able to answer the ownership transfer', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, inviteAndJoinMember } = await createOrg(); - const { member } = await inviteAndJoinMember(); - const { memberToken: lonelyMemberToken } = await inviteAndJoinMember(); +test.concurrent( + 'non-member should not be able to answer the ownership transfer', + async ({ expect }) => { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { organization, inviteAndJoinMember } = await createOrg(); + const { member } = await inviteAndJoinMember(); + const { memberToken: lonelyMemberToken } = await inviteAndJoinMember(); - const requestTransferResult = await requestOrganizationTransfer( - { - organization: organization.cleanId, - user: member.user.id, - }, - ownerToken, - ).then(r => r.expectNoGraphQLErrors()); + const requestTransferResult = await requestOrganizationTransfer( + { + organization: organization.cleanId, + user: member.user.id, + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); - const code = requestTransferResult.requestOrganizationTransfer.ok?.code; + const code = requestTransferResult.requestOrganizationTransfer.ok?.code; - if (!code) { - throw new Error('Could not create transfer request'); - } + if (!code) { + throw new Error('Could not create transfer request'); + } - const answerResult = await answerOrganizationTransferRequest( - { - organization: organization.cleanId, - code, - accept: true, - }, - lonelyMemberToken, - ).then(r => r.expectNoGraphQLErrors()); + const answerResult = await answerOrganizationTransferRequest( + { + organization: organization.cleanId, + code, + accept: true, + }, + lonelyMemberToken, + ).then(r => r.expectNoGraphQLErrors()); - expect(answerResult.answerOrganizationTransferRequest.error?.message).toBeDefined(); -}); + expect(answerResult.answerOrganizationTransferRequest.error?.message).toBeDefined(); + }, +); -test.concurrent('owner should not be able to answer the ownership transfer', async () => { +test.concurrent('owner should not be able to answer the ownership transfer', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, inviteAndJoinMember } = await createOrg(); const { member } = await inviteAndJoinMember(); @@ -268,41 +283,44 @@ test.concurrent('owner should not be able to answer the ownership transfer', asy expect(answerResult.answerOrganizationTransferRequest.error?.message).toBeDefined(); }); -test.concurrent('non-member should not be able to answer the ownership transfer', async () => { - const { createOrg, ownerToken } = await initSeed().createOwner(); - const { organization, inviteAndJoinMember } = await createOrg(); - const { member } = await inviteAndJoinMember(); - - const requestTransferResult = await requestOrganizationTransfer( - { - organization: organization.cleanId, - user: member.user.id, - }, - ownerToken, - ).then(r => r.expectNoGraphQLErrors()); - - const code = requestTransferResult.requestOrganizationTransfer.ok?.code; - - if (!code) { - throw new Error('Could not create transfer request'); - } - - const { ownerToken: nonMemberToken } = await initSeed().createOwner(); - const answerResult = await answerOrganizationTransferRequest( - { - organization: organization.cleanId, - code, - accept: true, - }, - nonMemberToken, - ).then(r => r.expectNoGraphQLErrors()); - - expect(answerResult.answerOrganizationTransferRequest.error?.message).toBeDefined(); -}); - +test.concurrent( + 'non-member should not be able to answer the ownership transfer', + async ({ expect }) => { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { organization, inviteAndJoinMember } = await createOrg(); + const { member } = await inviteAndJoinMember(); + + const requestTransferResult = await requestOrganizationTransfer( + { + organization: organization.cleanId, + user: member.user.id, + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); + + const code = requestTransferResult.requestOrganizationTransfer.ok?.code; + + if (!code) { + throw new Error('Could not create transfer request'); + } + + const { ownerToken: nonMemberToken } = await initSeed().createOwner(); + const answerResult = await answerOrganizationTransferRequest( + { + organization: organization.cleanId, + code, + accept: true, + }, + nonMemberToken, + ).then(r => r.expectNoGraphQLErrors()); + + expect(answerResult.answerOrganizationTransferRequest.error?.message).toBeDefined(); + }, +); + test.concurrent( 'previous owner should keep the ownership until the new owner accepts the transfer', - async () => { + async ({ expect }) => { const { createOrg, ownerToken, ownerEmail } = await initSeed().createOwner(); const { organization, inviteAndJoinMember, members } = await createOrg(); const { member } = await inviteAndJoinMember(); @@ -350,7 +368,7 @@ test.concurrent( test.concurrent( 'previous owner should have an Admin role, new owner should get an Admin role as well', - async () => { + async ({ expect }) => { const { createOrg, ownerToken, ownerEmail } = await initSeed().createOwner(); const { organization, inviteAndJoinMember, members } = await createOrg(); const { member, memberToken } = await inviteAndJoinMember(); diff --git a/integration-tests/tests/api/policy/policy-access.spec.ts b/integration-tests/tests/api/policy/policy-access.spec.ts index cf79009ea..90ac4f4a9 100644 --- a/integration-tests/tests/api/policy/policy-access.spec.ts +++ b/integration-tests/tests/api/policy/policy-access.spec.ts @@ -19,7 +19,7 @@ describe('Policy Access', () => { test.concurrent( 'should successfully fetch Target.schemaPolicy if the user has access to SETTINGS', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization, createProject, inviteAndJoinMember } = await createOrg(); const { project, target } = await createProject(ProjectType.Single); @@ -53,7 +53,7 @@ describe('Policy Access', () => { test.concurrent( 'should fail to fetch Target.schemaPolicy if the user lacks access to SETTINGS', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization, createProject, inviteAndJoinMember } = await createOrg(); const { project, target } = await createProject(ProjectType.Single); @@ -87,7 +87,7 @@ describe('Policy Access', () => { test.concurrent( 'should successfully fetch Project.schemaPolicy if the user has access to SETTINGS', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization, createProject, inviteAndJoinMember } = await createOrg(); const { project } = await createProject(ProjectType.Single); @@ -120,7 +120,7 @@ describe('Policy Access', () => { test.concurrent( 'should fail to fetch Project.schemaPolicy if the user lacks access to SETTINGS', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization, createProject, inviteAndJoinMember } = await createOrg(); const { project, target } = await createProject(ProjectType.Single); @@ -154,7 +154,7 @@ describe('Policy Access', () => { `); test.concurrent( 'should successfully fetch Organization.schemaPolicy if the user has access to SETTINGS', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization, inviteAndJoinMember } = await createOrg(); const adminRole = organization.memberRoles.find(r => r.name === 'Admin'); @@ -185,7 +185,7 @@ describe('Policy Access', () => { test.concurrent( 'should fail to fetch Organization.schemaPolicy if the user lacks access to SETTINGS', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization, inviteAndJoinMember } = await createOrg(); const { memberToken } = await inviteAndJoinMember(); diff --git a/integration-tests/tests/api/policy/policy-crud.spec.ts b/integration-tests/tests/api/policy/policy-crud.spec.ts index c9b1e033c..cde62a5c5 100644 --- a/integration-tests/tests/api/policy/policy-crud.spec.ts +++ b/integration-tests/tests/api/policy/policy-crud.spec.ts @@ -16,7 +16,7 @@ describe('Policy CRUD', () => { describe('Target level', () => { test.concurrent( 'Should return empty policy when project and org does not have one', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, target } = await createProject(ProjectType.Single); @@ -233,7 +233,7 @@ describe('Policy CRUD', () => { describe('Project level', () => { test.concurrent( 'creating a project should NOT create a record in the database for the policy', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); await createProject(ProjectType.Single); @@ -329,7 +329,7 @@ describe('Policy CRUD', () => { describe('Org level', () => { test.concurrent( 'creating a org should NOT create a record in the database for the policy', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); await createProject(ProjectType.Single); @@ -347,7 +347,7 @@ describe('Policy CRUD', () => { }, ); - test.concurrent('invalid rule name is rejected with an error', async () => { + test.concurrent('invalid rule name is rejected with an error', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, setOrganizationSchemaPolicy } = await createOrg(); await createProject(ProjectType.Single); @@ -358,7 +358,7 @@ describe('Policy CRUD', () => { expect(upsertResult.error?.message).toContain('Unknown rule name passed'); }); - test.concurrent('invalid rule config is rejected with an error', async () => { + test.concurrent('invalid rule config is rejected with an error', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, setOrganizationSchemaPolicy } = await createOrg(); await createProject(ProjectType.Single); @@ -371,7 +371,7 @@ describe('Policy CRUD', () => { ); }); - test.concurrent('empty rule config is rejected with an error', async () => { + test.concurrent('empty rule config is rejected with an error', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, setOrganizationSchemaPolicy } = await createOrg(); await createProject(ProjectType.Single); diff --git a/integration-tests/tests/api/project/crud.spec.ts b/integration-tests/tests/api/project/crud.spec.ts index de37b539c..92b475450 100644 --- a/integration-tests/tests/api/project/crud.spec.ts +++ b/integration-tests/tests/api/project/crud.spec.ts @@ -4,7 +4,7 @@ import { initSeed } from '../../../testkit/seed'; test.concurrent( 'creating a project should result in creating the development, staging and production targets', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { targets } = await createProject(ProjectType.Single); @@ -31,7 +31,7 @@ test.concurrent( }, ); -test.concurrent('renaming a project should result changing its cleanId', async () => { +test.concurrent('renaming a project should result changing its cleanId', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { project } = await createProject(ProjectType.Single); diff --git a/integration-tests/tests/api/rate-limit/emails.spec.ts b/integration-tests/tests/api/rate-limit/emails.spec.ts index 73eba4119..48efb1e2b 100644 --- a/integration-tests/tests/api/rate-limit/emails.spec.ts +++ b/integration-tests/tests/api/rate-limit/emails.spec.ts @@ -57,7 +57,7 @@ test('rate limit approaching and reached for organization', async () => { const collectResult = await collectOperations(new Array(10).fill(op)); expect(collectResult.status).toEqual(200); - await waitFor(5000); + await waitFor(8000); let sent = await emails.history(); expect(sent).toContainEqual({ diff --git a/integration-tests/tests/api/schema/check.spec.ts b/integration-tests/tests/api/schema/check.spec.ts index 23f42cf91..c3f1db452 100644 --- a/integration-tests/tests/api/schema/check.spec.ts +++ b/integration-tests/tests/api/schema/check.spec.ts @@ -4,7 +4,7 @@ import { execute } from '../../../testkit/graphql'; import { initSeed } from '../../../testkit/seed'; import { createPolicy } from '../policy/policy-check.spec'; -test.concurrent('can check a schema with target:registry:read access', async () => { +test.concurrent('can check a schema with target:registry:read access', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -65,7 +65,7 @@ test.concurrent('can check a schema with target:registry:read access', async () expect(checkResultValid.schemaCheck.__typename).toBe('SchemaCheckSuccess'); }); -test.concurrent('should match indentation of previous description', async () => { +test.concurrent('should match indentation of previous description', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -244,152 +244,158 @@ const ApproveFailedSchemaCheckMutation = graphql(/* GraphQL */ ` } `); -test.concurrent('successful check without previously published schema is persisted', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject, organization } = await createOrg(); - const { createToken, project, target } = await createProject(ProjectType.Single); +test.concurrent( + 'successful check without previously published schema is persisted', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject, organization } = await createOrg(); + const { createToken, project, target } = await createProject(ProjectType.Single); - // Create a token with read rights - const readToken = await createToken({ - targetScopes: [TargetAccessScope.RegistryRead], - projectScopes: [], - organizationScopes: [], - }); + // Create a token with read rights + const readToken = await createToken({ + targetScopes: [TargetAccessScope.RegistryRead], + projectScopes: [], + organizationScopes: [], + }); - // Check schema with read rights - const checkResult = await readToken - .checkSchema(/* GraphQL */ ` - type Query { - ping: String - pong: String - } - `) - .then(r => r.expectNoGraphQLErrors()); - const check = checkResult.schemaCheck; - - if (check.__typename !== 'SchemaCheckSuccess') { - throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`); - } - - const schemaCheckId = check.schemaCheck?.id; - - if (schemaCheckId == null) { - throw new Error('Missing schema check id.'); - } - - const schemaCheck = await execute({ - document: SchemaCheckQuery, - variables: { - selector: { - organization: organization.cleanId, - project: project.cleanId, - target: target.cleanId, - }, - id: schemaCheckId, - }, - authToken: readToken.secret, - }).then(r => r.expectNoGraphQLErrors()); - - expect(schemaCheck).toMatchObject({ - target: { - schemaCheck: { - __typename: 'SuccessfulSchemaCheck', - id: schemaCheckId, - createdAt: expect.any(String), - serviceName: null, - schemaVersion: null, - }, - }, - }); -}); - -test.concurrent('successful check with previously published schema is persisted', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject, organization } = await createOrg(); - const { createToken, project, target } = await createProject(ProjectType.Single); - - // Create a token with write rights - const writeToken = await createToken({ - targetScopes: [ - TargetAccessScope.Read, - TargetAccessScope.RegistryRead, - TargetAccessScope.RegistryWrite, - TargetAccessScope.Settings, - ], - }); - - // Publish schema with write rights - const publishResult = await writeToken - .publishSchema({ - sdl: /* GraphQL */ ` + // Check schema with read rights + const checkResult = await readToken + .checkSchema(/* GraphQL */ ` type Query { ping: String pong: String } - `, - }) - .then(r => r.expectNoGraphQLErrors()); + `) + .then(r => r.expectNoGraphQLErrors()); + const check = checkResult.schemaCheck; - // Schema publish should be successful - expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + if (check.__typename !== 'SchemaCheckSuccess') { + throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`); + } - // Create a token with read rights - const readToken = await createToken({ - targetScopes: [TargetAccessScope.RegistryRead], - projectScopes: [], - organizationScopes: [], - }); + const schemaCheckId = check.schemaCheck?.id; - // Check schema with read rights - const checkResult = await readToken - .checkSchema(/* GraphQL */ ` - type Query { - ping: String - pong: String - } - `) - .then(r => r.expectNoGraphQLErrors()); - const check = checkResult.schemaCheck; + if (schemaCheckId == null) { + throw new Error('Missing schema check id.'); + } - if (check.__typename !== 'SchemaCheckSuccess') { - throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`); - } - - const schemaCheckId = check.schemaCheck?.id; - - if (schemaCheckId == null) { - throw new Error('Missing schema check id.'); - } - - const schemaCheck = await execute({ - document: SchemaCheckQuery, - variables: { - selector: { - organization: organization.cleanId, - project: project.cleanId, - target: target.cleanId, - }, - id: schemaCheckId, - }, - authToken: readToken.secret, - }).then(r => r.expectNoGraphQLErrors()); - - expect(schemaCheck).toMatchObject({ - target: { - schemaCheck: { - __typename: 'SuccessfulSchemaCheck', + const schemaCheck = await execute({ + document: SchemaCheckQuery, + variables: { + selector: { + organization: organization.cleanId, + project: project.cleanId, + target: target.cleanId, + }, id: schemaCheckId, - createdAt: expect.any(String), - serviceName: null, - schemaVersion: { - id: expect.any(String), + }, + authToken: readToken.secret, + }).then(r => r.expectNoGraphQLErrors()); + + expect(schemaCheck).toMatchObject({ + target: { + schemaCheck: { + __typename: 'SuccessfulSchemaCheck', + id: schemaCheckId, + createdAt: expect.any(String), + serviceName: null, + schemaVersion: null, }, }, - }, - }); -}); + }); + }, +); -test.concurrent('failed check due to graphql validation is persisted', async () => { +test.concurrent( + 'successful check with previously published schema is persisted', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject, organization } = await createOrg(); + const { createToken, project, target } = await createProject(ProjectType.Single); + + // Create a token with write rights + const writeToken = await createToken({ + targetScopes: [ + TargetAccessScope.Read, + TargetAccessScope.RegistryRead, + TargetAccessScope.RegistryWrite, + TargetAccessScope.Settings, + ], + }); + + // Publish schema with write rights + const publishResult = await writeToken + .publishSchema({ + sdl: /* GraphQL */ ` + type Query { + ping: String + pong: String + } + `, + }) + .then(r => r.expectNoGraphQLErrors()); + + // Schema publish should be successful + expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + + // Create a token with read rights + const readToken = await createToken({ + targetScopes: [TargetAccessScope.RegistryRead], + projectScopes: [], + organizationScopes: [], + }); + + // Check schema with read rights + const checkResult = await readToken + .checkSchema(/* GraphQL */ ` + type Query { + ping: String + pong: String + } + `) + .then(r => r.expectNoGraphQLErrors()); + const check = checkResult.schemaCheck; + + if (check.__typename !== 'SchemaCheckSuccess') { + throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`); + } + + const schemaCheckId = check.schemaCheck?.id; + + if (schemaCheckId == null) { + throw new Error('Missing schema check id.'); + } + + const schemaCheck = await execute({ + document: SchemaCheckQuery, + variables: { + selector: { + organization: organization.cleanId, + project: project.cleanId, + target: target.cleanId, + }, + id: schemaCheckId, + }, + authToken: readToken.secret, + }).then(r => r.expectNoGraphQLErrors()); + + expect(schemaCheck).toMatchObject({ + target: { + schemaCheck: { + __typename: 'SuccessfulSchemaCheck', + id: schemaCheckId, + createdAt: expect.any(String), + serviceName: null, + schemaVersion: { + id: expect.any(String), + }, + }, + }, + }); + }, +); + +test.concurrent('failed check due to graphql validation is persisted', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(ProjectType.Single); @@ -454,7 +460,7 @@ test.concurrent('failed check due to graphql validation is persisted', async () }); }); -test.concurrent('failed check due to breaking change is persisted', async () => { +test.concurrent('failed check due to breaking change is persisted', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(ProjectType.Single); @@ -546,7 +552,7 @@ test.concurrent('failed check due to breaking change is persisted', async () => }); }); -test.concurrent('failed check due to policy error is persisted', async () => { +test.concurrent('failed check due to policy error is persisted', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target, setProjectSchemaPolicy } = await createProject( @@ -655,123 +661,126 @@ test.concurrent('failed check due to policy error is persisted', async () => { }); }); -test.concurrent('successful check with warnings and safe changes is persisted', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject, organization } = await createOrg(); - const { createToken, project, target, setProjectSchemaPolicy } = await createProject( - ProjectType.Single, - ); +test.concurrent( + 'successful check with warnings and safe changes is persisted', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject, organization } = await createOrg(); + const { createToken, project, target, setProjectSchemaPolicy } = await createProject( + ProjectType.Single, + ); - await setProjectSchemaPolicy(createPolicy(RuleInstanceSeverityLevel.Warning)); + await setProjectSchemaPolicy(createPolicy(RuleInstanceSeverityLevel.Warning)); - // Create a token with write rights - const writeToken = await createToken({ - targetScopes: [ - TargetAccessScope.Read, - TargetAccessScope.RegistryRead, - TargetAccessScope.RegistryWrite, - TargetAccessScope.Settings, - ], - }); + // Create a token with write rights + const writeToken = await createToken({ + targetScopes: [ + TargetAccessScope.Read, + TargetAccessScope.RegistryRead, + TargetAccessScope.RegistryWrite, + TargetAccessScope.Settings, + ], + }); - // Publish schema with write rights - const publishResult = await writeToken - .publishSchema({ - sdl: /* GraphQL */ ` + // Publish schema with write rights + const publishResult = await writeToken + .publishSchema({ + sdl: /* GraphQL */ ` + type Query { + ping: String + } + `, + }) + .then(r => r.expectNoGraphQLErrors()); + + // Schema publish should be successful + expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + + // Create a token with read rights + const readToken = await createToken({ + targetScopes: [TargetAccessScope.RegistryRead], + projectScopes: [], + organizationScopes: [], + }); + + // Check schema with read rights + const checkResult = await readToken + .checkSchema(/* GraphQL */ ` type Query { ping: String + foo: String } - `, - }) - .then(r => r.expectNoGraphQLErrors()); + `) + .then(r => r.expectNoGraphQLErrors()); + const check = checkResult.schemaCheck; - // Schema publish should be successful - expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + if (check.__typename !== 'SchemaCheckSuccess') { + throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`); + } - // Create a token with read rights - const readToken = await createToken({ - targetScopes: [TargetAccessScope.RegistryRead], - projectScopes: [], - organizationScopes: [], - }); + const schemaCheckId = check.schemaCheck?.id; - // Check schema with read rights - const checkResult = await readToken - .checkSchema(/* GraphQL */ ` - type Query { - ping: String - foo: String - } - `) - .then(r => r.expectNoGraphQLErrors()); - const check = checkResult.schemaCheck; + if (schemaCheckId == null) { + throw new Error('Missing schema check id.'); + } - if (check.__typename !== 'SchemaCheckSuccess') { - throw new Error(`Expected SchemaCheckSuccess, got ${check.__typename}`); - } - - const schemaCheckId = check.schemaCheck?.id; - - if (schemaCheckId == null) { - throw new Error('Missing schema check id.'); - } - - const schemaCheck = await execute({ - document: SchemaCheckQuery, - variables: { - selector: { - organization: organization.cleanId, - project: project.cleanId, - target: target.cleanId, - }, - id: schemaCheckId, - }, - authToken: readToken.secret, - }).then(r => r.expectNoGraphQLErrors()); - - expect(schemaCheck).toMatchObject({ - target: { - schemaCheck: { - __typename: 'SuccessfulSchemaCheck', - id: schemaCheckId, - createdAt: expect.any(String), - serviceName: null, - schemaVersion: { - id: expect.any(String), + const schemaCheck = await execute({ + document: SchemaCheckQuery, + variables: { + selector: { + organization: organization.cleanId, + project: project.cleanId, + target: target.cleanId, }, - schemaPolicyWarnings: { - edges: [ - { - node: { - end: { - column: 17, - line: 2, - }, - message: 'Description is required for type "Query"', - ruleId: 'require-description', - start: { - column: 12, - line: 2, + id: schemaCheckId, + }, + authToken: readToken.secret, + }).then(r => r.expectNoGraphQLErrors()); + + expect(schemaCheck).toMatchObject({ + target: { + schemaCheck: { + __typename: 'SuccessfulSchemaCheck', + id: schemaCheckId, + createdAt: expect.any(String), + serviceName: null, + schemaVersion: { + id: expect.any(String), + }, + schemaPolicyWarnings: { + edges: [ + { + node: { + end: { + column: expect.any(Number), + line: expect.any(Number), + }, + message: 'Description is required for type "Query"', + ruleId: 'require-description', + start: { + column: expect.any(Number), + line: expect.any(Number), + }, }, }, - }, - ], - }, - safeSchemaChanges: { - nodes: [ - { - message: "Field 'foo' was added to object type 'Query'", - }, - ], + ], + }, + safeSchemaChanges: { + nodes: [ + { + message: "Field 'foo' was added to object type 'Query'", + }, + ], + }, }, }, - }, - }); -}); + }); + }, +); test.concurrent( 'failed check due to missing service name is not persisted (federation/stitching)', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Federation); @@ -801,7 +810,7 @@ test.concurrent( }, ); -test.concurrent('metadata is persisted', async () => { +test.concurrent('metadata is persisted', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(ProjectType.Single); @@ -873,7 +882,7 @@ test.concurrent('metadata is persisted', async () => { test.concurrent( 'approve failed schema check that has breaking change status to successful and attaches meta information to the breaking change', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(ProjectType.Single); @@ -995,7 +1004,7 @@ test.concurrent( }, ); -test.concurrent('approve failed schema check with a comment', async () => { +test.concurrent('approve failed schema check with a comment', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(ProjectType.Single); @@ -1106,7 +1115,7 @@ test.concurrent('approve failed schema check with a comment', async () => { test.concurrent( 'approving a schema check with contextId containing breaking changes allows the changes for subsequent checks with the same contextId', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(ProjectType.Single); @@ -1256,7 +1265,7 @@ test.concurrent( test.concurrent( 'approving a schema check with contextId containing breaking changes does not allow the changes for subsequent checks with a different contextId', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(ProjectType.Single); @@ -1399,7 +1408,7 @@ test.concurrent( test.concurrent( 'subsequent schema check with shared contextId that contains new breaking changes that have not been approved fails', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(ProjectType.Single); @@ -1552,72 +1561,75 @@ test.concurrent( }, ); -test.concurrent('contextId that has more than 300 characters is not allowed', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject } = await createOrg(); - const { createToken } = await createProject(ProjectType.Single); +test.concurrent( + 'contextId that has more than 300 characters is not allowed', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject } = await createOrg(); + const { createToken } = await createProject(ProjectType.Single); - // Create a token with write rights - const writeToken = await createToken({ - targetScopes: [ - TargetAccessScope.Read, - TargetAccessScope.RegistryRead, - TargetAccessScope.RegistryWrite, - TargetAccessScope.Settings, - ], - }); - - // Publish schema with write rights - const publishResult = await writeToken - .publishSchema({ - sdl: /* GraphQL */ ` - type Query { - ping: String - pong: String - } - `, - }) - .then(r => r.expectNoGraphQLErrors()); - - // Schema publish should be successful - expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); - - // Create a token with read rights - const readToken = await createToken({ - targetScopes: [TargetAccessScope.RegistryRead], - projectScopes: [], - organizationScopes: [], - }); - - const contextId = ''; - - // Check schema with read rights - const checkResult = await readToken - .checkSchema( - /* GraphQL */ ` - type Query { - ping: Float - } - `, - undefined, - undefined, - contextId, - ) - .then(r => r.expectNoGraphQLErrors()); - - expect(checkResult.schemaCheck).toMatchObject({ - __typename: 'SchemaCheckError', - errors: { - nodes: [ - { - message: 'Context ID must be at least 1 character long.', - }, + // Create a token with write rights + const writeToken = await createToken({ + targetScopes: [ + TargetAccessScope.Read, + TargetAccessScope.RegistryRead, + TargetAccessScope.RegistryWrite, + TargetAccessScope.Settings, ], - }, - }); -}); + }); -test.concurrent('contextId that has fewer than 1 characters is not allowed', async () => { + // Publish schema with write rights + const publishResult = await writeToken + .publishSchema({ + sdl: /* GraphQL */ ` + type Query { + ping: String + pong: String + } + `, + }) + .then(r => r.expectNoGraphQLErrors()); + + // Schema publish should be successful + expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + + // Create a token with read rights + const readToken = await createToken({ + targetScopes: [TargetAccessScope.RegistryRead], + projectScopes: [], + organizationScopes: [], + }); + + const contextId = ''; + + // Check schema with read rights + const checkResult = await readToken + .checkSchema( + /* GraphQL */ ` + type Query { + ping: Float + } + `, + undefined, + undefined, + contextId, + ) + .then(r => r.expectNoGraphQLErrors()); + + expect(checkResult.schemaCheck).toMatchObject({ + __typename: 'SchemaCheckError', + errors: { + nodes: [ + { + message: 'Context ID must be at least 1 character long.', + }, + ], + }, + }); + }, +); + +test.concurrent('contextId that has fewer than 1 characters is not allowed', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); diff --git a/integration-tests/tests/api/schema/composite.spec.ts b/integration-tests/tests/api/schema/composite.spec.ts index c2741de72..bd2b03754 100644 --- a/integration-tests/tests/api/schema/composite.spec.ts +++ b/integration-tests/tests/api/schema/composite.spec.ts @@ -8,7 +8,7 @@ describe.each` ${ProjectType.Stitching} | ${'legacy'} ${ProjectType.Federation} | ${'legacy'} `('$projectType ($model)', ({ projectType, model }) => { - test.concurrent('should insert lowercase service name to DB', async () => { + test.concurrent('should insert lowercase service name to DB', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(projectType, { @@ -141,7 +141,7 @@ describe.each` `('$projectType', ({ projectType }) => { test.concurrent( 'should publish A, publish B, delete B, publish A and have A and B at the end', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(projectType); diff --git a/integration-tests/tests/api/schema/composition-federation-2.spec.ts b/integration-tests/tests/api/schema/composition-federation-2.spec.ts index 08080812f..8f27795ab 100644 --- a/integration-tests/tests/api/schema/composition-federation-2.spec.ts +++ b/integration-tests/tests/api/schema/composition-federation-2.spec.ts @@ -1,13 +1,13 @@ import { ProjectAccessScope, ProjectType, TargetAccessScope } from 'testkit/gql/graphql'; import { enableExternalSchemaComposition } from '../../../testkit/flow'; import { initSeed } from '../../../testkit/seed'; -import { generateUnique } from '../../../testkit/utils'; +import { generateUnique, getServiceHost } from '../../../testkit/utils'; // We do not resolve this to a host address, because we are calling this through a different flow: // GraphQL API -> Schema service -> Composition service -const dockerAddress = `composition_federation_2:3069`; +const dockerAddress = await getServiceHost('composition_federation_2', 3069, false); -test.concurrent('call an external service to compose and validate services', async () => { +test.concurrent('call an external service to compose and validate services', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, setNativeFederation } = await createProject(ProjectType.Federation); diff --git a/integration-tests/tests/api/schema/contracts-check.spec.ts b/integration-tests/tests/api/schema/contracts-check.spec.ts index f36514865..3e516c5ad 100644 --- a/integration-tests/tests/api/schema/contracts-check.spec.ts +++ b/integration-tests/tests/api/schema/contracts-check.spec.ts @@ -393,7 +393,7 @@ const SchemaCheckQuery = graphql(/* GraphQL */ ` test.concurrent( 'approve failed schema check that has breaking change in contract check -> updates the status to successful and attaches meta information to the breaking change', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization, setFeatureFlag } = await createOrg(); const { createToken, project, target, setNativeFederation } = await createProject( @@ -566,7 +566,7 @@ test.concurrent( test.concurrent( 'approving a schema check with contextId containing breaking changes allows the changes for subsequent checks with the same contextId', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization, setFeatureFlag } = await createOrg(); const { createToken, project, target, setNativeFederation } = await createProject( @@ -771,7 +771,7 @@ test.concurrent( test.concurrent( 'approving a schema check with contextId containing breaking changes does not allow the changes for subsequent checks with a different contextId', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization, setFeatureFlag } = await createOrg(); const { createToken, project, target, setNativeFederation } = await createProject( @@ -969,7 +969,7 @@ test.concurrent( test.concurrent( 'subsequent schema check with shared contextId that contains new breaking changes that have not been approved fails', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization, setFeatureFlag } = await createOrg(); const { createToken, project, target, setNativeFederation } = await createProject( @@ -1178,7 +1178,7 @@ test.concurrent( test.concurrent( 'schema check that has no composition errors in contract check -> can be approved', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization, setFeatureFlag } = await createOrg(); const { createToken, project, target, setNativeFederation } = await createProject( @@ -1296,7 +1296,7 @@ test.concurrent( test.concurrent( 'schema check that has composition errors in contract check -> can not be approved', - async () => { + async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { createProject, organization, setFeatureFlag } = await createOrg(); const { createToken, project, target, setNativeFederation } = await createProject( diff --git a/integration-tests/tests/api/schema/delete.spec.ts b/integration-tests/tests/api/schema/delete.spec.ts index 7397225a2..929060a5c 100644 --- a/integration-tests/tests/api/schema/delete.spec.ts +++ b/integration-tests/tests/api/schema/delete.spec.ts @@ -3,6 +3,7 @@ import { parse, print } from 'graphql'; import { enableExternalSchemaComposition } from 'testkit/flow'; import { ProjectAccessScope, ProjectType, TargetAccessScope } from 'testkit/gql/graphql'; import { initSeed } from 'testkit/seed'; +import { getServiceHost } from 'testkit/utils'; import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { createStorage } from '@hive/storage'; import { sortSDL } from '@theguild/federation-composition'; @@ -63,7 +64,7 @@ function normalizeSDL(sdl: string): string { test.concurrent( 'can delete a service and updates the CDN when the super schema is still composable', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken, target } = await createProject(ProjectType.Federation); @@ -245,7 +246,7 @@ test.concurrent( await enableExternalSchemaComposition( { - endpoint: `http://${`composition_federation_2:3069`}/compose`, + endpoint: `http://${await getServiceHost('composition_federation_2', 3069, false)}/compose`, // eslint-disable-next-line no-process-env secret: process.env.EXTERNAL_COMPOSITION_SECRET!, project: project.cleanId, diff --git a/integration-tests/tests/api/schema/external-composition.spec.ts b/integration-tests/tests/api/schema/external-composition.spec.ts index 57c689d2a..a61f8216e 100644 --- a/integration-tests/tests/api/schema/external-composition.spec.ts +++ b/integration-tests/tests/api/schema/external-composition.spec.ts @@ -1,10 +1,10 @@ import { ProjectAccessScope, ProjectType, TargetAccessScope } from 'testkit/gql/graphql'; -import { history, serviceName, servicePort } from '../../../testkit/external-composition'; +import { history } from '../../../testkit/external-composition'; import { enableExternalSchemaComposition } from '../../../testkit/flow'; import { initSeed } from '../../../testkit/seed'; -import { generateUnique } from '../../../testkit/utils'; +import { generateUnique, getServiceHost } from '../../../testkit/utils'; -test.concurrent('call an external service to compose and validate services', async () => { +test.concurrent('call an external service to compose and validate services', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, setNativeFederation } = await createProject(ProjectType.Federation); @@ -43,7 +43,7 @@ test.concurrent('call an external service to compose and validate services', asy // we use internal docker network to connect to the external composition service, // so we need to use the name and not resolved host - const dockerAddress = `${serviceName}:${servicePort}`; + const dockerAddress = await getServiceHost('external_composition', 3012, false); // enable external composition const externalCompositionResult = await enableExternalSchemaComposition( { @@ -91,7 +91,7 @@ test.concurrent('call an external service to compose and validate services', asy test.concurrent( 'an expected error coming from the external composition service should be visible to the user', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, setNativeFederation } = await createProject( @@ -131,7 +131,7 @@ test.concurrent( // we use internal docker network to connect to the external composition service, // so we need to use the name and not resolved host - const dockerAddress = `${serviceName}:${servicePort}`; + const dockerAddress = await getServiceHost('external_composition', 3012, false); // enable external composition const externalCompositionResult = await enableExternalSchemaComposition( { @@ -191,7 +191,7 @@ test.concurrent( test.concurrent( 'a network error coming from the external composition service should be visible to the user', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, setNativeFederation } = await createProject( @@ -231,7 +231,7 @@ test.concurrent( // we use internal docker network to connect to the external composition service, // so we need to use the name and not resolved host - const dockerAddress = `${serviceName}:${servicePort}`; + const dockerAddress = await getServiceHost('external_composition', 3012, false); // enable external composition const externalCompositionResult = await enableExternalSchemaComposition( { @@ -326,7 +326,7 @@ test.concurrent('a timeout error should be visible to the user', async ({ expect // we use internal docker network to connect to the external composition service, // so we need to use the name and not resolved host - const dockerAddress = `${serviceName}:${servicePort}`; + const dockerAddress = await getServiceHost('external_composition', 3012, false); // enable external composition const externalCompositionResult = await enableExternalSchemaComposition( { diff --git a/integration-tests/tests/api/schema/publish.spec.ts b/integration-tests/tests/api/schema/publish.spec.ts index 074b94fcb..cdd6d48b4 100644 --- a/integration-tests/tests/api/schema/publish.spec.ts +++ b/integration-tests/tests/api/schema/publish.spec.ts @@ -4,6 +4,7 @@ import { graphql } from 'testkit/gql'; /* eslint-disable no-process-env */ import { ProjectAccessScope, ProjectType, TargetAccessScope } from 'testkit/gql/graphql'; import { execute } from 'testkit/graphql'; +import { getServiceHost } from 'testkit/utils'; // eslint-disable-next-line import/no-extraneous-dependencies import { createStorage } from '@hive/storage'; import { @@ -13,31 +14,34 @@ import { } from '../../../testkit/flow'; import { initSeed } from '../../../testkit/seed'; -test.concurrent('cannot publish a schema without target:registry:write access', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject } = await createOrg(); - const { createToken } = await createProject(ProjectType.Federation); - const readToken = await createToken({ - targetScopes: [TargetAccessScope.RegistryRead], - projectScopes: [], - organizationScopes: [], - }); +test.concurrent( + 'cannot publish a schema without target:registry:write access', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject } = await createOrg(); + const { createToken } = await createProject(ProjectType.Federation); + const readToken = await createToken({ + targetScopes: [TargetAccessScope.RegistryRead], + projectScopes: [], + organizationScopes: [], + }); - const resultErrors = await readToken - .publishSchema({ - sdl: /* GraphQL */ ` - type Query { - ping: String - } - `, - }) - .then(r => r.expectGraphQLErrors()); + const resultErrors = await readToken + .publishSchema({ + sdl: /* GraphQL */ ` + type Query { + ping: String + } + `, + }) + .then(r => r.expectGraphQLErrors()); - expect(resultErrors).toHaveLength(1); - expect(resultErrors[0].message).toMatch('target:registry:write'); -}); + expect(resultErrors).toHaveLength(1); + expect(resultErrors[0].message).toMatch('target:registry:write'); + }, +); -test.concurrent('can publish a schema with target:registry:write access', async () => { +test.concurrent('can publish a schema with target:registry:write access', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -76,63 +80,66 @@ test.concurrent('can publish a schema with target:registry:write access', async expect(versionsResult).toHaveLength(2); }); -test.concurrent('base schema should not affect the output schema persisted in db', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject } = await createOrg(); - const { createToken } = await createProject(ProjectType.Single); - const readWriteToken = await createToken({ - targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], - projectScopes: [], - organizationScopes: [], - }); +test.concurrent( + 'base schema should not affect the output schema persisted in db', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject } = await createOrg(); + const { createToken } = await createProject(ProjectType.Single); + const readWriteToken = await createToken({ + targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], + projectScopes: [], + organizationScopes: [], + }); - // Publish schema with write rights - const publishResult = await readWriteToken - .publishSchema({ - commit: '1', - sdl: `type Query { ping: String }`, - }) - .then(r => r.expectNoGraphQLErrors()); + // Publish schema with write rights + const publishResult = await readWriteToken + .publishSchema({ + commit: '1', + sdl: `type Query { ping: String }`, + }) + .then(r => r.expectNoGraphQLErrors()); - // Schema publish should be successful - expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + // Schema publish should be successful + expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); - await readWriteToken.updateBaseSchema(` + await readWriteToken.updateBaseSchema(` directive @auth on OBJECT | FIELD_DEFINITION `); - const extendedPublishResult = await readWriteToken - .publishSchema({ - commit: '2', - sdl: `type Query { ping: String @auth pong: String }`, - }) - .then(r => r.expectNoGraphQLErrors()); - expect(extendedPublishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + const extendedPublishResult = await readWriteToken + .publishSchema({ + commit: '2', + sdl: `type Query { ping: String @auth pong: String }`, + }) + .then(r => r.expectNoGraphQLErrors()); + expect(extendedPublishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); - const versionsResult = await readWriteToken.fetchVersions(5); - expect(versionsResult).toHaveLength(2); + const versionsResult = await readWriteToken.fetchVersions(5); + expect(versionsResult).toHaveLength(2); - const latestResult = await readWriteToken.latestSchema(); - expect(latestResult.latestVersion?.schemas.total).toBe(1); + const latestResult = await readWriteToken.latestSchema(); + expect(latestResult.latestVersion?.schemas.total).toBe(1); - const firstNode = latestResult.latestVersion?.schemas.nodes[0]; + const firstNode = latestResult.latestVersion?.schemas.nodes[0]; - expect(firstNode).toEqual( - expect.objectContaining({ - commit: '2', - source: expect.stringContaining('type Query { ping: String @auth pong: String }'), - }), - ); - expect(firstNode).not.toEqual( - expect.objectContaining({ - source: expect.stringContaining('directive'), - }), - ); + expect(firstNode).toEqual( + expect.objectContaining({ + commit: '2', + source: expect.stringContaining('type Query { ping: String @auth pong: String }'), + }), + ); + expect(firstNode).not.toEqual( + expect.objectContaining({ + source: expect.stringContaining('directive'), + }), + ); - expect(latestResult.latestVersion?.baseSchema).toMatch( - 'directive @auth on OBJECT | FIELD_DEFINITION', - ); -}); + expect(latestResult.latestVersion?.baseSchema).toMatch( + 'directive @auth on OBJECT | FIELD_DEFINITION', + ); + }, +); test.concurrent.each(['legacy', 'modern'])( 'directives should not be removed (federation %s)', @@ -266,7 +273,7 @@ test.concurrent.each(['legacy', 'modern'])( }, ); -test.concurrent('share publication of schema using redis', async () => { +test.concurrent('share publication of schema using redis', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Federation); @@ -318,7 +325,7 @@ test.concurrent('share publication of schema using redis', async () => { await expect(readWriteToken.fetchVersions(3)).resolves.toHaveLength(2); }); -test.concurrent('CDN data can not be fetched with an invalid access token', async () => { +test.concurrent('CDN data can not be fetched with an invalid access token', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -351,7 +358,7 @@ test.concurrent('CDN data can not be fetched with an invalid access token', asyn expect(res.status).toEqual(403); }); -test.concurrent('CDN data can be fetched with an valid access token', async () => { +test.concurrent('CDN data can be fetched with an valid access token', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -386,7 +393,7 @@ test.concurrent('CDN data can be fetched with an valid access token', async () = expect(cdnResult.status).toEqual(200); }); -test.concurrent('cannot do API request with invalid access token', async () => { +test.concurrent('cannot do API request with invalid access token', async ({ expect }) => { const errors = await publishSchema( { commit: '1', @@ -407,7 +414,7 @@ test.concurrent('cannot do API request with invalid access token', async () => { test.concurrent( 'should publish only one schema if multiple same publishes are started in parallel', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -457,43 +464,46 @@ describe.each` }; const serviceUrl = projectType === ProjectType.Single ? {} : { url: 'http://localhost:4000' }; - test.concurrent('linkToWebsite should be available when publishing initial schema', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject, organization } = await createOrg(); - const { project, target, createToken } = await createProject(projectType, { - useLegacyRegistryModels: model === 'legacy', - }); - const readWriteToken = await createToken({ - targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], - projectScopes: [], - organizationScopes: [], - }); + test.concurrent( + 'linkToWebsite should be available when publishing initial schema', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject, organization } = await createOrg(); + const { project, target, createToken } = await createProject(projectType, { + useLegacyRegistryModels: model === 'legacy', + }); + const readWriteToken = await createToken({ + targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], + projectScopes: [], + organizationScopes: [], + }); - const result = await readWriteToken - .publishSchema({ - author: 'Kamil', - commit: 'abc123', - sdl: `type Query { ping: String }`, - ...serviceName, - ...serviceUrl, - }) - .then(r => r.expectNoGraphQLErrors()); + const result = await readWriteToken + .publishSchema({ + author: 'Kamil', + commit: 'abc123', + sdl: `type Query { ping: String }`, + ...serviceName, + ...serviceUrl, + }) + .then(r => r.expectNoGraphQLErrors()); - expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + expect(result.schemaPublish.__typename).toBe('SchemaPublishSuccess'); - const linkToWebsite = - result.schemaPublish.__typename === 'SchemaPublishSuccess' - ? result.schemaPublish.linkToWebsite - : null; + const linkToWebsite = + result.schemaPublish.__typename === 'SchemaPublishSuccess' + ? result.schemaPublish.linkToWebsite + : null; - expect(linkToWebsite).toEqual( - `${process.env.HIVE_APP_BASE_URL}/${organization.cleanId}/${project.cleanId}/${target.cleanId}`, - ); - }); + expect(linkToWebsite).toEqual( + `${process.env.HIVE_APP_BASE_URL}/${organization.cleanId}/${project.cleanId}/${target.cleanId}`, + ); + }, + ); test.concurrent( 'linkToWebsite should be available when publishing non-initial schema', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { createToken, project, target } = await createProject(projectType, { @@ -635,7 +645,7 @@ describe('schema publishing changes are persisted', () => { /** Only provide if you want to test a service url change */ serviceUrlAfter?: string; }) { - test.concurrent(`[Schema change] ${args.name}`, async () => { + test.concurrent(`[Schema change] ${args.name}`, async ({ expect }) => { const serviceName = { service: 'test', }; @@ -2819,7 +2829,7 @@ test('Target.schemaVersion: result is read from the database', async () => { test('Composition Error (Federation 2) can be served from the database', async () => { const storage = await createStorage(connectionString(), 1); - const dockerAddress = `composition_federation_2:3069`; + const serviceAddress = await getServiceHost('composition_federation_2', 3069, false); try { const initialSchema = /* GraphQL */ ` @@ -2880,7 +2890,7 @@ test('Composition Error (Federation 2) can be served from the database', async ( await enableExternalSchemaComposition( { - endpoint: `http://${dockerAddress}/compose`, + endpoint: `http://${serviceAddress}/compose`, // eslint-disable-next-line no-process-env secret: process.env.EXTERNAL_COMPOSITION_SECRET!, project: project.cleanId, @@ -2949,7 +2959,7 @@ test('Composition Error (Federation 2) can be served from the database', async ( test('Composition Network Failure (Federation 2)', async () => { const storage = await createStorage(connectionString(), 1); - const dockerAddress = `composition_federation_2:3069`; + const serviceAddress = await getServiceHost('composition_federation_2', 3069, false); try { const initialSchema = /* GraphQL */ ` @@ -3009,7 +3019,7 @@ test('Composition Network Failure (Federation 2)', async () => { await enableExternalSchemaComposition( { - endpoint: `http://${dockerAddress}/compose`, + endpoint: `http://${serviceAddress}/compose`, // eslint-disable-next-line no-process-env secret: process.env.EXTERNAL_COMPOSITION_SECRET!, project: project.cleanId, @@ -3050,7 +3060,7 @@ test('Composition Network Failure (Federation 2)', async () => { await enableExternalSchemaComposition( { - endpoint: `http://${dockerAddress}/no_compose`, + endpoint: `http://${serviceAddress}/no_compose`, secret: process.env.EXTERNAL_COMPOSITION_SECRET!, project: project.cleanId, organization: organization.cleanId, @@ -3574,7 +3584,7 @@ test.concurrent( test.concurrent( 'publishing Federation schema results in tags stored on the schema version', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, setFeatureFlag } = await createOrg(); const { createToken, setNativeFederation } = await createProject(ProjectType.Federation); @@ -3609,7 +3619,7 @@ test.concurrent( }, ); -test.concurrent('CDN services are published in alphanumeric order', async () => { +test.concurrent('CDN services are published in alphanumeric order', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Stitching); @@ -3670,7 +3680,7 @@ test.concurrent('CDN services are published in alphanumeric order', async () => test.concurrent( 'Composite schema project publish without service name results in error', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Federation); @@ -3701,7 +3711,7 @@ test.concurrent( test.concurrent( 'Composite schema project publish without service url results in error', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Federation); diff --git a/integration-tests/tests/api/target/crud.spec.ts b/integration-tests/tests/api/target/crud.spec.ts index 3744bc13b..fa517d1d5 100644 --- a/integration-tests/tests/api/target/crud.spec.ts +++ b/integration-tests/tests/api/target/crud.spec.ts @@ -2,7 +2,7 @@ import { ProjectType } from 'testkit/gql/graphql'; import { renameTarget } from '../../../testkit/flow'; import { initSeed } from '../../../testkit/seed'; -test.concurrent('renaming a target should result changing its cleanId', async () => { +test.concurrent('renaming a target should result changing its cleanId', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, target } = await createProject(ProjectType.Single); diff --git a/integration-tests/tests/api/target/tokens.spec.ts b/integration-tests/tests/api/target/tokens.spec.ts index e21f160b1..223a95261 100644 --- a/integration-tests/tests/api/target/tokens.spec.ts +++ b/integration-tests/tests/api/target/tokens.spec.ts @@ -3,7 +3,7 @@ import { initSeed } from '../../../testkit/seed'; test.concurrent( 'setting no scopes equals to readonly for organization, project, target', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -43,22 +43,25 @@ test.concurrent( }, ); -test.concurrent('cannot set a scope on a token if user has no access to that scope', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject, inviteAndJoinMember } = await createOrg(); - const { createToken, target } = await createProject(ProjectType.Single); - const { memberToken } = await inviteAndJoinMember(); +test.concurrent( + 'cannot set a scope on a token if user has no access to that scope', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject, inviteAndJoinMember } = await createOrg(); + const { createToken, target } = await createProject(ProjectType.Single); + const { memberToken } = await inviteAndJoinMember(); - // member should not have access to target:registry:write (as it's only a Viewer) - const tokenResult = createToken({ - targetScopes: [TargetAccessScope.RegistryWrite], - projectScopes: [], - organizationScopes: [], - target, - actorToken: memberToken, - }); + // member should not have access to target:registry:write (as it's only a Viewer) + const tokenResult = createToken({ + targetScopes: [TargetAccessScope.RegistryWrite], + projectScopes: [], + organizationScopes: [], + target, + actorToken: memberToken, + }); - await expect(tokenResult).rejects.toThrowError( - 'No access (reason: "Missing target:tokens:write permission")', - ); -}); + await expect(tokenResult).rejects.toThrowError( + 'No access (reason: "Missing target:tokens:write permission")', + ); + }, +); diff --git a/integration-tests/tests/api/target/usage.spec.ts b/integration-tests/tests/api/target/usage.spec.ts index 2f65d6f28..576d7d6c3 100644 --- a/integration-tests/tests/api/target/usage.spec.ts +++ b/integration-tests/tests/api/target/usage.spec.ts @@ -48,7 +48,7 @@ function prepareBatch(amount: number, operation: CollectedOperation) { test.concurrent( 'collect operation and publish schema using WRITE access but read operations and check schema using READ access', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -108,7 +108,7 @@ test.concurrent( }, ]); expect(collectResult.status).toEqual(200); - await waitFor(5000); + await waitFor(8000); // should be breaking because the field is used now const usedCheckResult = await readToken @@ -143,21 +143,23 @@ test.concurrent( }, ); -test.concurrent('normalize and collect operation without breaking its syntax', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject } = await createOrg(); - const { createToken } = await createProject(ProjectType.Single); - const writeToken = await createToken({ - targetScopes: [ - TargetAccessScope.Read, - TargetAccessScope.RegistryRead, - TargetAccessScope.RegistryWrite, - ], - projectScopes: [ProjectAccessScope.Read], - organizationScopes: [OrganizationAccessScope.Read], - }); +test.concurrent( + 'normalize and collect operation without breaking its syntax', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject } = await createOrg(); + const { createToken } = await createProject(ProjectType.Single); + const writeToken = await createToken({ + targetScopes: [ + TargetAccessScope.Read, + TargetAccessScope.RegistryRead, + TargetAccessScope.RegistryWrite, + ], + projectScopes: [ProjectAccessScope.Read], + organizationScopes: [OrganizationAccessScope.Read], + }); - const raw_document = ` + const raw_document = ` query outfit { recommendations( input: { @@ -210,64 +212,65 @@ test.concurrent('normalize and collect operation without breaking its syntax', a } `; - const normalized_document = normalizeOperation({ - document: parse(raw_document), - operationName: 'outfit', - hideLiterals: true, - removeAliases: true, - }); - - const collectResult = await writeToken.collectLegacyOperations([ - { - operation: normalizeOperation({ - document: parse(raw_document), - operationName: 'outfit', - hideLiterals: true, - removeAliases: true, - }), + const normalized_document = normalizeOperation({ + document: parse(raw_document), operationName: 'outfit', - fields: ['Query', 'Query.ping'], - execution: { - ok: true, - duration: 200_000_000, - errorsTotal: 0, + hideLiterals: true, + removeAliases: true, + }); + + const collectResult = await writeToken.collectLegacyOperations([ + { + operation: normalizeOperation({ + document: parse(raw_document), + operationName: 'outfit', + hideLiterals: true, + removeAliases: true, + }), + operationName: 'outfit', + fields: ['Query', 'Query.ping'], + execution: { + ok: true, + duration: 200_000_000, + errorsTotal: 0, + }, }, - }, - ]); - expect(collectResult.status).toEqual(200); - await waitFor(5000); + ]); + expect(collectResult.status).toEqual(200); + await waitFor(8000); - const from = formatISO(subHours(Date.now(), 6)); - const to = formatISO(Date.now()); - const operationsStats = await writeToken.readOperationsStats(from, to); - expect(operationsStats.operations.nodes).toHaveLength(1); + const from = formatISO(subHours(Date.now(), 6)); + const to = formatISO(Date.now()); + const operationsStats = await writeToken.readOperationsStats(from, to); + expect(operationsStats.operations.nodes).toHaveLength(1); - const op = operationsStats.operations.nodes[0]; - expect(op.count).toEqual(1); + const op = operationsStats.operations.nodes[0]; + expect(op.count).toEqual(1); - const doc = await writeToken.readOperationBody(op.operationHash!); + const doc = await writeToken.readOperationBody(op.operationHash!); - if (!doc) { - throw new Error('Operation body is empty'); - } + if (!doc) { + throw new Error('Operation body is empty'); + } - expect(() => { - parse(doc); - }).not.toThrow(); - expect(print(parse(doc))).toEqual(print(parse(normalized_document))); - expect(op.operationHash).toBeDefined(); - expect(op.duration.p75).toEqual(200); - expect(op.duration.p90).toEqual(200); - expect(op.duration.p95).toEqual(200); - expect(op.duration.p99).toEqual(200); - expect(op.kind).toEqual('query'); - expect(op.name).toMatch('outfit'); - expect(op.percentage).toBeGreaterThan(99); -}); + expect(() => { + parse(doc); + }).not.toThrow(); + expect(print(parse(doc))).toEqual(print(parse(normalized_document))); + expect(op.operationHash).toBeDefined(); + expect(op.duration.p75).toEqual(200); + expect(op.duration.p90).toEqual(200); + expect(op.duration.p95).toEqual(200); + expect(op.duration.p99).toEqual(200); + expect(op.kind).toEqual('query'); + expect(op.name).toMatch('outfit'); + expect(op.percentage).toBeGreaterThan(99); + }, +); test.concurrent( 'number of produced and collected operations should match (no errors)', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { createToken } = await createProject(ProjectType.Single); @@ -299,7 +302,7 @@ test.concurrent( ); } - await waitFor(5000); + await waitFor(8000); const from = formatISO(subHours(Date.now(), 6)); const to = formatISO(Date.now()); @@ -324,7 +327,7 @@ test.concurrent( }, ); -test.concurrent('check usage from two selected targets', async () => { +test.concurrent('check usage from two selected targets', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, target: staging, createToken } = await createProject(ProjectType.Single); @@ -416,7 +419,7 @@ test.concurrent('check usage from two selected targets', async () => { ]); expect(collectResult.status).toEqual(200); - await waitFor(5000); + await waitFor(8000); // should not be breaking because the field is unused on staging // ping is used but on production @@ -465,7 +468,7 @@ test.concurrent('check usage from two selected targets', async () => { expect(usedCheckResult.schemaCheck.valid).toEqual(true); }); -test.concurrent('check usage not from excluded client names', async () => { +test.concurrent('check usage not from excluded client names', async ({ expect }) => { const { createOrg, ownerToken } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, target, createToken } = await createProject(ProjectType.Single); @@ -566,7 +569,7 @@ test.concurrent('check usage not from excluded client names', async () => { }, ]); expect(collectResult.status).toEqual(200); - await waitFor(5000); + await waitFor(8000); // should be breaking because the field is used // Query.me would be removed, but was requested by cli and app @@ -663,7 +666,7 @@ describe('changes with usage data', () => { afterReportedOperation: 'SchemaCheckSuccess' | 'SchemaCheckError'; }; }) { - test.concurrent(input.title, async () => { + test.concurrent(input.title, async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { target, createToken } = await createProject(ProjectType.Single); @@ -735,7 +738,7 @@ describe('changes with usage data', () => { ]); expect(collectResult.status).toEqual(200); - await waitFor(5000); + await waitFor(8000); await expect( token @@ -1116,7 +1119,7 @@ describe('changes with usage data', () => { }); }); -test.concurrent('number of produced and collected operations should match', async () => { +test.concurrent('number of produced and collected operations should match', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { target, createToken } = await createProject(ProjectType.Single); @@ -1168,7 +1171,7 @@ test.concurrent('number of produced and collected operations should match', asyn ); } - await waitFor(5000); + await waitFor(10000); const result = await clickHouseQuery<{ target: string; @@ -1207,7 +1210,7 @@ test.concurrent('number of produced and collected operations should match', asyn test.concurrent( 'different order of schema coordinates should not result in different hash', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { target, createToken } = await createProject(ProjectType.Single); @@ -1244,7 +1247,7 @@ test.concurrent( }, ]); - await waitFor(5000); + await waitFor(8000); const coordinatesResult = await clickHouseQuery<{ target: string; @@ -1270,7 +1273,7 @@ test.concurrent( test.concurrent( 'same operation but with different schema coordinates should result in different hash', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { target, createToken } = await createProject(ProjectType.Single); @@ -1307,7 +1310,7 @@ test.concurrent( }, ]); - await waitFor(5000); + await waitFor(8000); const coordinatesResult = await clickHouseQuery<{ coordinate: string; @@ -1337,7 +1340,7 @@ test.concurrent( test.concurrent( 'operations with the same schema coordinates and body but with different name should result in different hashes', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { target, createToken } = await createProject(ProjectType.Single); @@ -1374,7 +1377,7 @@ test.concurrent( }, ]); - await waitFor(5000); + await waitFor(8000); const coordinatesResult = await clickHouseQuery<{ target: string; @@ -1398,7 +1401,7 @@ test.concurrent( }, ); -test.concurrent('ignore operations with syntax errors', async () => { +test.concurrent('ignore operations with syntax errors', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject } = await createOrg(); const { target, createToken } = await createProject(ProjectType.Single); @@ -1443,7 +1446,7 @@ test.concurrent('ignore operations with syntax errors', async () => { }), ); - await waitFor(5000); + await waitFor(8000); const coordinatesResult = await clickHouseQuery<{ target: string; @@ -1466,7 +1469,7 @@ test.concurrent('ignore operations with syntax errors', async () => { expect(operationsResult.rows).toEqual(1); }); -test.concurrent('ensure correct data', async () => { +test.concurrent('ensure correct data', async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { createProject, organization } = await createOrg(); const { target, createToken } = await createProject(ProjectType.Single); @@ -1512,7 +1515,7 @@ test.concurrent('ensure correct data', async () => { }, ]); - await waitFor(5000); + await waitFor(8000); // operation_collection const operationCollectionResult = await clickHouseQuery<{ @@ -1784,67 +1787,69 @@ test.concurrent('ensure correct data', async () => { ).toBe(organization.rateLimit.retentionInDays); }); -test.concurrent('ensure correct data when data retention period is non-default', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject, setDataRetention } = await createOrg(); - const { target, createToken } = await createProject(ProjectType.Single); - const writeToken = await createToken({ - targetScopes: [ - TargetAccessScope.Read, - TargetAccessScope.RegistryRead, - TargetAccessScope.RegistryWrite, - ], - projectScopes: [ProjectAccessScope.Read], - organizationScopes: [OrganizationAccessScope.Read], - }); +test.concurrent( + 'ensure correct data when data retention period is non-default', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject, setDataRetention } = await createOrg(); + const { target, createToken } = await createProject(ProjectType.Single); + const writeToken = await createToken({ + targetScopes: [ + TargetAccessScope.Read, + TargetAccessScope.RegistryRead, + TargetAccessScope.RegistryWrite, + ], + projectScopes: [ProjectAccessScope.Read], + organizationScopes: [OrganizationAccessScope.Read], + }); - const dataRetentionInDays = 60; - await setDataRetention(dataRetentionInDays); - await waitFor(6_000); // so the data retention is propagated to the rate-limiter + const dataRetentionInDays = 60; + await setDataRetention(dataRetentionInDays); + await waitFor(10_000); // so the data retention is propagated to the rate-limiter - await writeToken.collectLegacyOperations([ - { - operation: 'query ping { ping }', // those spaces are expected and important to ensure normalization is in place - operationName: 'ping', - fields: ['Query', 'Query.ping'], - execution: { - ok: true, - duration: 200_000_000, - errorsTotal: 0, - }, - }, - { - operation: 'query ping { ping }', - operationName: 'ping', - fields: ['Query', 'Query.ping'], - execution: { - ok: true, - duration: 200_000_000, - errorsTotal: 0, - }, - metadata: { - client: { - name: 'test-name', - version: 'test-version', + await writeToken.collectLegacyOperations([ + { + operation: 'query ping { ping }', // those spaces are expected and important to ensure normalization is in place + operationName: 'ping', + fields: ['Query', 'Query.ping'], + execution: { + ok: true, + duration: 200_000_000, + errorsTotal: 0, }, }, - }, - ]); + { + operation: 'query ping { ping }', + operationName: 'ping', + fields: ['Query', 'Query.ping'], + execution: { + ok: true, + duration: 200_000_000, + errorsTotal: 0, + }, + metadata: { + client: { + name: 'test-name', + version: 'test-version', + }, + }, + }, + ]); - await waitFor(5000); + await waitFor(8000); - // operation_collection - const operationCollectionResult = await clickHouseQuery<{ - target: string; - hash: string; - name: string; - body: string; - operation_kind: string; - coordinates: string[]; - total: string; - timestamp: string; - expires_at: string; - }>(` + // operation_collection + const operationCollectionResult = await clickHouseQuery<{ + target: string; + hash: string; + name: string; + body: string; + operation_kind: string; + coordinates: string[]; + total: string; + timestamp: string; + expires_at: string; + }>(` SELECT target, hash, @@ -1860,36 +1865,36 @@ test.concurrent('ensure correct data when data retention period is non-default', GROUP BY target, hash, coordinates, name, body, operation_kind, timestamp, expires_at `); - expect(operationCollectionResult.data).toHaveLength(1); + expect(operationCollectionResult.data).toHaveLength(1); - const operationCollectionRow = operationCollectionResult.data[0]; - expect(operationCollectionRow.body).toEqual('query ping{ping}'); - expect(operationCollectionRow.coordinates).toHaveLength(2); - expect(operationCollectionRow.coordinates).toContainEqual('Query.ping'); - expect(operationCollectionRow.coordinates).toContainEqual('Query'); - expect(operationCollectionRow.hash).toHaveLength(32); - expect(operationCollectionRow.name).toBe('ping'); - expect(operationCollectionRow.target).toBe(target.id); - expect(ensureNumber(operationCollectionRow.total)).toEqual(2); - expect( - differenceInDays( - parseClickHouseDate(operationCollectionRow.expires_at), - parseClickHouseDate(operationCollectionRow.timestamp), - ), - ).toBe(dataRetentionInDays); + const operationCollectionRow = operationCollectionResult.data[0]; + expect(operationCollectionRow.body).toEqual('query ping{ping}'); + expect(operationCollectionRow.coordinates).toHaveLength(2); + expect(operationCollectionRow.coordinates).toContainEqual('Query.ping'); + expect(operationCollectionRow.coordinates).toContainEqual('Query'); + expect(operationCollectionRow.hash).toHaveLength(32); + expect(operationCollectionRow.name).toBe('ping'); + expect(operationCollectionRow.target).toBe(target.id); + expect(ensureNumber(operationCollectionRow.total)).toEqual(2); + expect( + differenceInDays( + parseClickHouseDate(operationCollectionRow.expires_at), + parseClickHouseDate(operationCollectionRow.timestamp), + ), + ).toBe(dataRetentionInDays); - // operations - const operationsResult = await clickHouseQuery<{ - target: string; - timestamp: string; - expires_at: string; - hash: string; - ok: boolean; - errors: number; - duration: number; - client_name: string; - client_version: string; - }>(` + // operations + const operationsResult = await clickHouseQuery<{ + target: string; + timestamp: string; + expires_at: string; + hash: string; + ok: boolean; + errors: number; + duration: number; + client_name: string; + client_version: string; + }>(` SELECT target, timestamp, @@ -1904,46 +1909,47 @@ test.concurrent('ensure correct data when data retention period is non-default', WHERE target = '${target.id}' `); - expect(operationsResult.data).toHaveLength(2); + expect(operationsResult.data).toHaveLength(2); - const operationWithClient = operationsResult.data.find(o => o.client_name.length > 0)!; - expect(operationWithClient).toBeDefined(); - expect(operationWithClient.client_name).toEqual('test-name'); - expect(operationWithClient.client_version).toEqual('test-version'); - expect(ensureNumber(operationWithClient.duration)).toEqual(200_000_000); - expect(ensureNumber(operationWithClient.errors)).toEqual(0); - expect(operationWithClient.hash).toHaveLength(32); - expect(operationWithClient.target).toEqual(target.id); - expect( - differenceInDays( - parseClickHouseDate(operationWithClient.expires_at), - parseClickHouseDate(operationWithClient.timestamp), - ), - ).toBe(dataRetentionInDays); + const operationWithClient = operationsResult.data.find(o => o.client_name.length > 0)!; + expect(operationWithClient).toBeDefined(); + expect(operationWithClient.client_name).toEqual('test-name'); + expect(operationWithClient.client_version).toEqual('test-version'); + expect(ensureNumber(operationWithClient.duration)).toEqual(200_000_000); + expect(ensureNumber(operationWithClient.errors)).toEqual(0); + expect(operationWithClient.hash).toHaveLength(32); + expect(operationWithClient.target).toEqual(target.id); + expect( + differenceInDays( + parseClickHouseDate(operationWithClient.expires_at), + parseClickHouseDate(operationWithClient.timestamp), + ), + ).toBe(dataRetentionInDays); - const operationWithoutClient = operationsResult.data.find(o => o.client_name.length === 0)!; - expect(operationWithoutClient).toBeDefined(); - expect(operationWithoutClient.client_name).toHaveLength(0); - expect(operationWithoutClient.client_version).toHaveLength(0); - expect(ensureNumber(operationWithoutClient.duration)).toEqual(200_000_000); - expect(ensureNumber(operationWithoutClient.errors)).toEqual(0); - expect(operationWithoutClient.hash).toHaveLength(32); - expect(operationWithoutClient.target).toEqual(target.id); - expect( - differenceInDays( - parseClickHouseDate(operationWithoutClient.expires_at), - parseClickHouseDate(operationWithoutClient.timestamp), - ), - ).toBe(dataRetentionInDays); + const operationWithoutClient = operationsResult.data.find(o => o.client_name.length === 0)!; + expect(operationWithoutClient).toBeDefined(); + expect(operationWithoutClient.client_name).toHaveLength(0); + expect(operationWithoutClient.client_version).toHaveLength(0); + expect(ensureNumber(operationWithoutClient.duration)).toEqual(200_000_000); + expect(ensureNumber(operationWithoutClient.errors)).toEqual(0); + expect(operationWithoutClient.hash).toHaveLength(32); + expect(operationWithoutClient.target).toEqual(target.id); + expect( + differenceInDays( + parseClickHouseDate(operationWithoutClient.expires_at), + parseClickHouseDate(operationWithoutClient.timestamp), + ), + ).toBe(dataRetentionInDays); - // operations_hourly - const operationsHourlyResult = await clickHouseQuery<{ - target: string; - hash: string; - total_ok: string; - total: string; - quantiles: [number]; - }>(` + await waitFor(3000); + // operations_hourly + const operationsHourlyResult = await clickHouseQuery<{ + target: string; + hash: string; + total_ok: string; + total: string; + quantiles: [number]; + }>(` SELECT target, sum(total) as total, @@ -1955,26 +1961,26 @@ test.concurrent('ensure correct data when data retention period is non-default', GROUP BY target, hash `); - expect(operationsHourlyResult.data).toHaveLength(1); + expect(operationsHourlyResult.data).toHaveLength(1); - const hourlyAgg = operationsHourlyResult.data[0]; - expect(hourlyAgg).toBeDefined(); - expect(ensureNumber(hourlyAgg.quantiles[0])).toEqual(200_000_000); - expect(ensureNumber(hourlyAgg.total)).toEqual(2); - expect(ensureNumber(hourlyAgg.total_ok)).toEqual(2); - expect(hourlyAgg.hash).toHaveLength(32); - expect(hourlyAgg.target).toEqual(target.id); + const hourlyAgg = operationsHourlyResult.data[0]; + expect(hourlyAgg).toBeDefined(); + expect(ensureNumber(hourlyAgg.quantiles[0])).toEqual(200_000_000); + expect(ensureNumber(hourlyAgg.total)).toEqual(2); + expect(ensureNumber(hourlyAgg.total_ok)).toEqual(2); + expect(hourlyAgg.hash).toHaveLength(32); + expect(hourlyAgg.target).toEqual(target.id); - // operations_daily - const operationsDailyResult = await clickHouseQuery<{ - target: string; - timestamp: string; - expires_at: string; - hash: string; - total_ok: string; - total: string; - quantiles: [number]; - }>(` + // operations_daily + const operationsDailyResult = await clickHouseQuery<{ + target: string; + timestamp: string; + expires_at: string; + hash: string; + total_ok: string; + total: string; + quantiles: [number]; + }>(` SELECT target, timestamp, @@ -1988,31 +1994,31 @@ test.concurrent('ensure correct data when data retention period is non-default', GROUP BY target, hash, timestamp, expires_at `); - expect(operationsDailyResult.data).toHaveLength(1); + expect(operationsDailyResult.data).toHaveLength(1); - const dailyAgg = operationsDailyResult.data[0]; - expect(dailyAgg).toBeDefined(); - expect(ensureNumber(dailyAgg.quantiles[0])).toEqual(200_000_000); - expect(ensureNumber(dailyAgg.total)).toEqual(2); - expect(ensureNumber(dailyAgg.total_ok)).toEqual(2); - expect(dailyAgg.hash).toHaveLength(32); - expect(dailyAgg.target).toEqual(target.id); - expect( - differenceInDays( - parseClickHouseDate(dailyAgg.expires_at), - parseClickHouseDate(dailyAgg.timestamp), - ), - ).toBe(dataRetentionInDays); + const dailyAgg = operationsDailyResult.data[0]; + expect(dailyAgg).toBeDefined(); + expect(ensureNumber(dailyAgg.quantiles[0])).toEqual(200_000_000); + expect(ensureNumber(dailyAgg.total)).toEqual(2); + expect(ensureNumber(dailyAgg.total_ok)).toEqual(2); + expect(dailyAgg.hash).toHaveLength(32); + expect(dailyAgg.target).toEqual(target.id); + expect( + differenceInDays( + parseClickHouseDate(dailyAgg.expires_at), + parseClickHouseDate(dailyAgg.timestamp), + ), + ).toBe(dataRetentionInDays); - // coordinates_daily - const coordinatesDailyResult = await clickHouseQuery<{ - target: string; - hash: string; - total: string; - coordinate: string; - timestamp: string; - expires_at: string; - }>(` + // coordinates_daily + const coordinatesDailyResult = await clickHouseQuery<{ + target: string; + hash: string; + total: string; + coordinate: string; + timestamp: string; + expires_at: string; + }>(` SELECT target, sum(total) as total, @@ -2025,42 +2031,42 @@ test.concurrent('ensure correct data when data retention period is non-default', GROUP BY target, hash, coordinate, timestamp, expires_at `); - expect(coordinatesDailyResult.data).toHaveLength(2); + expect(coordinatesDailyResult.data).toHaveLength(2); - const rootCoordinate = coordinatesDailyResult.data.find(c => c.coordinate === 'Query')!; - expect(rootCoordinate).toBeDefined(); - expect(ensureNumber(rootCoordinate.total)).toEqual(2); - expect(rootCoordinate.hash).toHaveLength(32); - expect(rootCoordinate.target).toEqual(target.id); - expect( - differenceInDays( - parseClickHouseDate(rootCoordinate.expires_at), - parseClickHouseDate(rootCoordinate.timestamp), - ), - ).toBe(dataRetentionInDays); + const rootCoordinate = coordinatesDailyResult.data.find(c => c.coordinate === 'Query')!; + expect(rootCoordinate).toBeDefined(); + expect(ensureNumber(rootCoordinate.total)).toEqual(2); + expect(rootCoordinate.hash).toHaveLength(32); + expect(rootCoordinate.target).toEqual(target.id); + expect( + differenceInDays( + parseClickHouseDate(rootCoordinate.expires_at), + parseClickHouseDate(rootCoordinate.timestamp), + ), + ).toBe(dataRetentionInDays); - const fieldCoordinate = coordinatesDailyResult.data.find(c => c.coordinate === 'Query.ping')!; - expect(fieldCoordinate).toBeDefined(); - expect(ensureNumber(fieldCoordinate.total)).toEqual(2); - expect(fieldCoordinate.hash).toHaveLength(32); - expect(fieldCoordinate.target).toEqual(target.id); - expect( - differenceInDays( - parseClickHouseDate(fieldCoordinate.expires_at), - parseClickHouseDate(fieldCoordinate.timestamp), - ), - ).toBe(dataRetentionInDays); + const fieldCoordinate = coordinatesDailyResult.data.find(c => c.coordinate === 'Query.ping')!; + expect(fieldCoordinate).toBeDefined(); + expect(ensureNumber(fieldCoordinate.total)).toEqual(2); + expect(fieldCoordinate.hash).toHaveLength(32); + expect(fieldCoordinate.target).toEqual(target.id); + expect( + differenceInDays( + parseClickHouseDate(fieldCoordinate.expires_at), + parseClickHouseDate(fieldCoordinate.timestamp), + ), + ).toBe(dataRetentionInDays); - // clients_daily - const clientsDailyResult = await clickHouseQuery<{ - target: string; - hash: string; - client_name: string; - client_version: string; - total: string; - timestamp: string; - expires_at: string; - }>(` + // clients_daily + const clientsDailyResult = await clickHouseQuery<{ + target: string; + hash: string; + client_name: string; + client_version: string; + total: string; + timestamp: string; + expires_at: string; + }>(` SELECT target, sum(total) as total, @@ -2074,34 +2080,37 @@ test.concurrent('ensure correct data when data retention period is non-default', GROUP BY target, hash, client_name, client_version, timestamp, expires_at `); - expect(clientsDailyResult.data).toHaveLength(2); + expect(clientsDailyResult.data).toHaveLength(2); - const dailyAggOfKnownClient = clientsDailyResult.data.find(c => c.client_name === 'test-name')!; - expect(dailyAggOfKnownClient).toBeDefined(); - expect(ensureNumber(dailyAggOfKnownClient.total)).toEqual(1); - expect(dailyAggOfKnownClient.client_version).toBe('test-version'); - expect(dailyAggOfKnownClient.hash).toHaveLength(32); - expect(dailyAggOfKnownClient.target).toEqual(target.id); - expect( - differenceInDays( - parseClickHouseDate(dailyAggOfKnownClient.expires_at), - parseClickHouseDate(dailyAggOfKnownClient.timestamp), - ), - ).toBe(dataRetentionInDays); + const dailyAggOfKnownClient = clientsDailyResult.data.find(c => c.client_name === 'test-name')!; + expect(dailyAggOfKnownClient).toBeDefined(); + expect(ensureNumber(dailyAggOfKnownClient.total)).toEqual(1); + expect(dailyAggOfKnownClient.client_version).toBe('test-version'); + expect(dailyAggOfKnownClient.hash).toHaveLength(32); + expect(dailyAggOfKnownClient.target).toEqual(target.id); + expect( + differenceInDays( + parseClickHouseDate(dailyAggOfKnownClient.expires_at), + parseClickHouseDate(dailyAggOfKnownClient.timestamp), + ), + ).toBe(dataRetentionInDays); - const dailyAggOfUnknownClient = clientsDailyResult.data.find(c => c.client_name !== 'test-name')!; - expect(dailyAggOfUnknownClient).toBeDefined(); - expect(ensureNumber(dailyAggOfUnknownClient.total)).toEqual(1); - expect(dailyAggOfUnknownClient.client_version).toHaveLength(0); - expect(dailyAggOfUnknownClient.hash).toHaveLength(32); - expect(dailyAggOfUnknownClient.target).toEqual(target.id); - expect( - differenceInDays( - parseClickHouseDate(dailyAggOfUnknownClient.expires_at), - parseClickHouseDate(dailyAggOfUnknownClient.timestamp), - ), - ).toBe(dataRetentionInDays); -}); + const dailyAggOfUnknownClient = clientsDailyResult.data.find( + c => c.client_name !== 'test-name', + )!; + expect(dailyAggOfUnknownClient).toBeDefined(); + expect(ensureNumber(dailyAggOfUnknownClient.total)).toEqual(1); + expect(dailyAggOfUnknownClient.client_version).toHaveLength(0); + expect(dailyAggOfUnknownClient.hash).toHaveLength(32); + expect(dailyAggOfUnknownClient.target).toEqual(target.id); + expect( + differenceInDays( + parseClickHouseDate(dailyAggOfUnknownClient.expires_at), + parseClickHouseDate(dailyAggOfUnknownClient.timestamp), + ), + ).toBe(dataRetentionInDays); + }, +); const SubscriptionSchemaCheckQuery = graphql(/* GraphQL */ ` query SubscriptionSchemaCheck($selector: TargetSelectorInput!, $id: ID!) { @@ -2144,227 +2153,230 @@ const SubscriptionSchemaCheckQuery = graphql(/* GraphQL */ ` } `); -test.concurrent('test threshold when using conditional breaking change detection', async () => { - const { createOrg } = await initSeed().createOwner(); - const { createProject } = await createOrg(); - const { createToken } = await createProject(ProjectType.Single); - const token = await createToken({ - targetScopes: [ - TargetAccessScope.Read, - TargetAccessScope.RegistryRead, - TargetAccessScope.RegistryWrite, - TargetAccessScope.Settings, - ], - projectScopes: [ProjectAccessScope.Read], - organizationScopes: [OrganizationAccessScope.Read], - }); +test.concurrent( + 'test threshold when using conditional breaking change detection', + async ({ expect }) => { + const { createOrg } = await initSeed().createOwner(); + const { createProject } = await createOrg(); + const { createToken } = await createProject(ProjectType.Single); + const token = await createToken({ + targetScopes: [ + TargetAccessScope.Read, + TargetAccessScope.RegistryRead, + TargetAccessScope.RegistryWrite, + TargetAccessScope.Settings, + ], + projectScopes: [ProjectAccessScope.Read], + organizationScopes: [OrganizationAccessScope.Read], + }); - const sdl = /* GraphQL */ ` - type Query { - a: String - b: String - c: String - } - `; + const sdl = /* GraphQL */ ` + type Query { + a: String + b: String + c: String + } + `; - const queryA = parse(/* GraphQL */ ` - query { - a - } - `); - const queryB = parse(/* GraphQL */ ` - query { - b - } - `); + const queryA = parse(/* GraphQL */ ` + query { + a + } + `); + const queryB = parse(/* GraphQL */ ` + query { + b + } + `); - function collectA() { - client.collectUsage()( - { - document: queryA, - schema, - contextValue: { - request, + function collectA() { + client.collectUsage()( + { + document: queryA, + schema, + contextValue: { + request, + }, }, - }, - {}, - ); - } + {}, + ); + } - function collectB() { - client.collectUsage()( - { - document: queryB, - schema, - contextValue: { - request, + function collectB() { + client.collectUsage()( + { + document: queryB, + schema, + contextValue: { + request, + }, }, - }, - {}, + {}, + ); + } + + const schema = buildASTSchema(parse(sdl)); + + const schemaPublishResult = await token + .publishSchema({ + sdl, + author: 'Kamil', + commit: 'initial', + }) + .then(res => res.expectNoGraphQLErrors()); + + expect(schemaPublishResult.schemaPublish.__typename).toEqual('SchemaPublishSuccess'); + + await token.toggleTargetValidation(true); + + const unused = await token + .checkSchema(/* GraphQL */ ` + type Query { + b: String + c: String + } + `) + .then(r => r.expectNoGraphQLErrors()); + + if (unused.schemaCheck.__typename !== 'SchemaCheckSuccess') { + throw new Error(`Expected SchemaCheckSuccess, got ${unused.schemaCheck.__typename}`); + } + + expect(unused.schemaCheck.changes).toEqual( + expect.objectContaining({ + nodes: expect.arrayContaining([ + expect.objectContaining({ + message: "Field 'a' was removed from object type 'Query' (non-breaking based on usage)", + }), + ]), + total: 1, + }), ); - } - const schema = buildASTSchema(parse(sdl)); + const usageAddress = await getServiceHost('usage', 8081); - const schemaPublishResult = await token - .publishSchema({ - sdl, - author: 'Kamil', - commit: 'initial', - }) - .then(res => res.expectNoGraphQLErrors()); - - expect(schemaPublishResult.schemaPublish.__typename).toEqual('SchemaPublishSuccess'); - - await token.toggleTargetValidation(true); - - const unused = await token - .checkSchema(/* GraphQL */ ` - type Query { - b: String - c: String - } - `) - .then(r => r.expectNoGraphQLErrors()); - - if (unused.schemaCheck.__typename !== 'SchemaCheckSuccess') { - throw new Error(`Expected SchemaCheckSuccess, got ${unused.schemaCheck.__typename}`); - } - - expect(unused.schemaCheck.changes).toEqual( - expect.objectContaining({ - nodes: expect.arrayContaining([ - expect.objectContaining({ - message: "Field 'a' was removed from object type 'Query' (non-breaking based on usage)", - }), - ]), - total: 1, - }), - ); - - 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': 'integration-tests', - 'x-graphql-client-version': '6.6.6', - }, - }); - - collectA(); - - await waitFor(5000); - - const used = await token - .checkSchema(/* GraphQL */ ` - type Query { - b: String - c: String - } - `) - .then(r => r.expectNoGraphQLErrors()); - - if (used.schemaCheck.__typename !== 'SchemaCheckError') { - throw new Error(`Expected SchemaCheckError, got ${used.schemaCheck.__typename}`); - } - - expect(used.schemaCheck.errors).toEqual({ - nodes: [ - { - message: "Field 'a' was removed from object type 'Query'", + const client = createHive({ + enabled: true, + token: token.secret, + usage: true, + debug: false, + agent: { + logger: createLogger('debug'), + maxSize: 1, }, - ], - total: 1, - }); - - // Now let's make Query.a below threshold by making 3 queries for Query.b - - collectB(); - collectB(); - collectB(); - - await token.updateTargetValidationSettings({ - excludedClients: [], - percentage: 50, - }); - - await waitFor(5000); - - const below = await token - .checkSchema(/* GraphQL */ ` - type Query { - b: String - c: String - } - `) - .then(r => r.expectNoGraphQLErrors()); - - if (below.schemaCheck.__typename !== 'SchemaCheckSuccess') { - throw new Error(`Expected SchemaCheckSuccess, got ${below.schemaCheck.__typename}`); - } - - expect(below.schemaCheck.changes).toEqual( - expect.objectContaining({ - nodes: expect.arrayContaining([ - expect.objectContaining({ - message: "Field 'a' was removed from object type 'Query' (non-breaking based on usage)", - }), - ]), - total: 1, - }), - ); - - // Make it above threshold again, by making 3 queries for Query.a - - collectA(); - collectA(); - collectA(); - - await waitFor(5000); - - const relevant = await token - .checkSchema(/* GraphQL */ ` - type Query { - b: String - c: String - } - `) - .then(r => r.expectNoGraphQLErrors()); - - if (relevant.schemaCheck.__typename !== 'SchemaCheckError') { - throw new Error(`Expected SchemaCheckError, got ${relevant.schemaCheck.__typename}`); - } - - expect(relevant.schemaCheck.errors).toEqual({ - nodes: [ - { - message: "Field 'a' was removed from object type 'Query'", + selfHosting: { + usageEndpoint: 'http://' + usageAddress, + graphqlEndpoint: 'http://noop/', + applicationUrl: 'http://noop/', }, - ], - total: 1, - }); -}); + }); + + const request = new Request('http://localhost:4000/graphql', { + method: 'POST', + headers: { + 'x-graphql-client-name': 'integration-tests', + 'x-graphql-client-version': '6.6.6', + }, + }); + + collectA(); + + await waitFor(8000); + + const used = await token + .checkSchema(/* GraphQL */ ` + type Query { + b: String + c: String + } + `) + .then(r => r.expectNoGraphQLErrors()); + + if (used.schemaCheck.__typename !== 'SchemaCheckError') { + throw new Error(`Expected SchemaCheckError, got ${used.schemaCheck.__typename}`); + } + + expect(used.schemaCheck.errors).toEqual({ + nodes: [ + { + message: "Field 'a' was removed from object type 'Query'", + }, + ], + total: 1, + }); + + // Now let's make Query.a below threshold by making 3 queries for Query.b + + collectB(); + collectB(); + collectB(); + + await token.updateTargetValidationSettings({ + excludedClients: [], + percentage: 50, + }); + + await waitFor(8000); + + const below = await token + .checkSchema(/* GraphQL */ ` + type Query { + b: String + c: String + } + `) + .then(r => r.expectNoGraphQLErrors()); + + if (below.schemaCheck.__typename !== 'SchemaCheckSuccess') { + throw new Error(`Expected SchemaCheckSuccess, got ${below.schemaCheck.__typename}`); + } + + expect(below.schemaCheck.changes).toEqual( + expect.objectContaining({ + nodes: expect.arrayContaining([ + expect.objectContaining({ + message: "Field 'a' was removed from object type 'Query' (non-breaking based on usage)", + }), + ]), + total: 1, + }), + ); + + // Make it above threshold again, by making 3 queries for Query.a + + collectA(); + collectA(); + collectA(); + + await waitFor(8000); + + const relevant = await token + .checkSchema(/* GraphQL */ ` + type Query { + b: String + c: String + } + `) + .then(r => r.expectNoGraphQLErrors()); + + if (relevant.schemaCheck.__typename !== 'SchemaCheckError') { + throw new Error(`Expected SchemaCheckError, got ${relevant.schemaCheck.__typename}`); + } + + expect(relevant.schemaCheck.errors).toEqual({ + nodes: [ + { + message: "Field 'a' was removed from object type 'Query'", + }, + ], + total: 1, + }); + }, +); test.concurrent( 'subscription operation is used for conditional breaking change detection', - async () => { + async ({ expect }) => { const { createOrg } = await initSeed().createOwner(); const { organization, createProject } = await createOrg(); const { project, target, createToken } = await createProject(ProjectType.Single); @@ -2474,7 +2486,7 @@ test.concurrent( }, }); - await waitFor(5000); + await waitFor(10000); const used = await token .checkSchema(/* GraphQL */ ` @@ -2594,7 +2606,7 @@ test.concurrent( percentage: 50, }); - await waitFor(5000); + await waitFor(8000); const irrelevant = await token .checkSchema(/* GraphQL */ ` @@ -2667,7 +2679,7 @@ test.concurrent( }, }); - await waitFor(5000); + await waitFor(8000); const relevant = await token .checkSchema(/* GraphQL */ ` diff --git a/integration-tests/tests/models/federation-legacy.spec.ts b/integration-tests/tests/models/federation-legacy.spec.ts index fc3e3712f..ab0800fe7 100644 --- a/integration-tests/tests/models/federation-legacy.spec.ts +++ b/integration-tests/tests/models/federation-legacy.spec.ts @@ -1,4 +1,5 @@ import { ProjectType, RegistryModel } from 'testkit/gql/graphql'; +import { normalizeCliOutput } from '../../../scripts/serializers/cli-output'; import { createCLI, schemaPublish } from '../../testkit/cli'; import { prepareProject } from '../../testkit/registry-models'; import { initSeed } from '../../testkit/seed'; @@ -267,48 +268,47 @@ describe('publish', () => { serviceUrl: 'http://products:3000/graphql', }; - await expect( - publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } + const out = publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } - type Product { - id: ID! - name: String! - } - `, - ...service, - expect: 'latest-composable', - }), - ).resolves.toMatchInlineSnapshot(` + type Product { + id: ID! + name: String! + } + `, + ...service, + expect: 'latest-composable', + }).then(normalizeCliOutput); + + await expect(out).resolves.toMatchInlineSnapshot(` v Published initial schema. - i Available at http://localhost:8080/$organization/$project/$target + i Available at $appUrl/$organization/$project/$target `); - await expect( - publish({ - sdl: /* GraphQL */ ` - type Query { - topProduct: Product - } + const out2 = publish({ + sdl: /* GraphQL */ ` + type Query { + topProduct: Product + } - type Product { - id: ID! - name: String! - price: Int! - } - `, - ...service, - expect: 'latest-composable', - }), - ).resolves.toMatchInlineSnapshot(` + type Product { + id: ID! + name: String! + price: Int! + } + `, + ...service, + expect: 'latest-composable', + }).then(normalizeCliOutput); + await expect(out2).resolves.toMatchInlineSnapshot(` i Detected 1 change Safe changes: - Field price was added to object type Product v Schema published - i Available at http://localhost:8080/$organization/$project/$target/history/$version + i Available at $appUrl/$organization/$project/$target/history/$version `); }); }); diff --git a/integration-tests/tests/models/federation-native-forceLegacyCompositionInTargets.spec.ts b/integration-tests/tests/models/federation-native-forceLegacyCompositionInTargets.spec.ts index a5c0e35aa..f4d78e8fe 100644 --- a/integration-tests/tests/models/federation-native-forceLegacyCompositionInTargets.spec.ts +++ b/integration-tests/tests/models/federation-native-forceLegacyCompositionInTargets.spec.ts @@ -301,9 +301,7 @@ describe('publish', () => { expect(output).toEqual(expect.stringContaining(`v Published initial schema.`)); expect(output).toEqual( - expect.stringContaining( - `i Available at http://localhost:8080/$organization/$project/$target`, - ), + expect.stringContaining(`i Available at $appUrl/$organization/$project/$target`), ); output = normalizeCliOutput( @@ -327,7 +325,7 @@ describe('publish', () => { expect(output).toEqual(expect.stringContaining(`v Schema published`)); expect(output).toEqual( expect.stringContaining( - `i Available at http://localhost:8080/$organization/$project/$target/history/$version`, + `i Available at $appUrl/$organization/$project/$target/history/$version`, ), ); }); diff --git a/integration-tests/tests/models/federation.spec.ts b/integration-tests/tests/models/federation.spec.ts index c683a420a..00b57b7cd 100644 --- a/integration-tests/tests/models/federation.spec.ts +++ b/integration-tests/tests/models/federation.spec.ts @@ -307,9 +307,7 @@ describe('publish', () => { expect(output).toEqual(expect.stringContaining(`v Published initial schema.`)); expect(output).toEqual( - expect.stringContaining( - `i Available at http://localhost:8080/$organization/$project/$target`, - ), + expect.stringContaining(`i Available at $appUrl/$organization/$project/$target`), ); output = normalizeCliOutput( @@ -333,7 +331,7 @@ describe('publish', () => { expect(output).toEqual(expect.stringContaining(`v Schema published`)); expect(output).toEqual( expect.stringContaining( - `i Available at http://localhost:8080/$organization/$project/$target/history/$version`, + `i Available at $appUrl/$organization/$project/$target/history/$version`, ), ); }); diff --git a/integration-tests/tests/models/single-legacy.spec.ts b/integration-tests/tests/models/single-legacy.spec.ts index 829fe02f1..d9050267e 100644 --- a/integration-tests/tests/models/single-legacy.spec.ts +++ b/integration-tests/tests/models/single-legacy.spec.ts @@ -149,7 +149,7 @@ describe('publish', () => { }), ).resolves.toMatchInlineSnapshot(` v Published initial schema. - i Available at http://localhost:8080/$organization/$project/$target + i Available at $appUrl/$organization/$project/$target `); await expect( @@ -172,7 +172,7 @@ describe('publish', () => { Safe changes: - Field price was added to object type Product v Schema published - i Available at http://localhost:8080/$organization/$project/$target/history/$version + i Available at $appUrl/$organization/$project/$target/history/$version `); }); }); diff --git a/integration-tests/tests/models/single.spec.ts b/integration-tests/tests/models/single.spec.ts index 15af7ee34..78826b6d0 100644 --- a/integration-tests/tests/models/single.spec.ts +++ b/integration-tests/tests/models/single.spec.ts @@ -143,9 +143,7 @@ describe('publish', () => { expect(output).toEqual(expect.stringContaining(`v Published initial schema.`)); expect(output).toEqual( - expect.stringContaining( - `i Available at http://localhost:8080/$organization/$project/$target`, - ), + expect.stringContaining(`i Available at $appUrl/$organization/$project/$target`), ); output = normalizeCliOutput( @@ -168,7 +166,7 @@ describe('publish', () => { expect(output).toEqual(expect.stringContaining(`v Schema published`)); expect(output).toEqual( expect.stringContaining( - `i Available at http://localhost:8080/$organization/$project/$target/history/$version`, + `i Available at $appUrl/$organization/$project/$target/history/$version`, ), ); }); diff --git a/integration-tests/tests/models/stitching-legacy.spec.ts b/integration-tests/tests/models/stitching-legacy.spec.ts index 0aab43b9d..3847e6c81 100644 --- a/integration-tests/tests/models/stitching-legacy.spec.ts +++ b/integration-tests/tests/models/stitching-legacy.spec.ts @@ -253,7 +253,7 @@ describe('publish', () => { }), ).resolves.toMatchInlineSnapshot(` v Published initial schema. - i Available at http://localhost:8080/$organization/$project/$target + i Available at $appUrl/$organization/$project/$target `); await expect( @@ -277,7 +277,7 @@ describe('publish', () => { Safe changes: - Field price was added to object type Product v Schema published - i Available at http://localhost:8080/$organization/$project/$target/history/$version + i Available at $appUrl/$organization/$project/$target/history/$version `); }); }); diff --git a/integration-tests/tests/models/stitching.spec.ts b/integration-tests/tests/models/stitching.spec.ts index 73a9c2864..9e0b800fc 100644 --- a/integration-tests/tests/models/stitching.spec.ts +++ b/integration-tests/tests/models/stitching.spec.ts @@ -268,9 +268,7 @@ describe('publish', () => { expect(output).toEqual(expect.stringContaining('v Published initial schema.')); expect(output).toEqual( - expect.stringContaining( - 'i Available at http://localhost:8080/$organization/$project/$target', - ), + expect.stringContaining('i Available at $appUrl/$organization/$project/$target'), ); output = normalizeCliOutput( @@ -294,7 +292,7 @@ describe('publish', () => { expect(output).toEqual(expect.stringContaining(`v Schema published`)); expect(output).toEqual( expect.stringContaining( - `i Available at http://localhost:8080/$organization/$project/$target/history/$version`, + `i Available at $appUrl/$organization/$project/$target/history/$version`, ), ); }); diff --git a/integration-tests/tests/schema/contracts.spec.ts b/integration-tests/tests/schema/contracts.spec.ts index cc958db20..086c3da80 100644 --- a/integration-tests/tests/schema/contracts.spec.ts +++ b/integration-tests/tests/schema/contracts.spec.ts @@ -1,7 +1,10 @@ +import { getServiceHost } from 'testkit/utils'; import type { SchemaBuilderApi } from '@hive/schema'; import { createTRPCProxyClient, httpLink } from '@trpc/client'; -const host = process.env['SCHEMA_SERVICE_HOST_OVERRIDE'] || 'http://localhost:3002'; +const host = + process.env['SCHEMA_SERVICE_HOST_OVERRIDE'] || + (await getServiceHost('schema', 3002).then(r => `http://${r}`)); const client = createTRPCProxyClient({ links: [ diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json index 4c55ad55f..2753647d0 100644 --- a/integration-tests/tsconfig.json +++ b/integration-tests/tsconfig.json @@ -12,5 +12,5 @@ } ] }, - "include": ["./testkit", "./tests", "./expect.ts"] + "include": ["./testkit", "./tests", "./expect.ts", "local-dev.ts"] } diff --git a/integration-tests/vite.config.ts b/integration-tests/vite.config.ts index 1a9163ece..5d5584cd6 100644 --- a/integration-tests/vite.config.ts +++ b/integration-tests/vite.config.ts @@ -1,5 +1,13 @@ import { defineConfig } from 'vitest/config'; +const setupFiles = ['../scripts/serializer.ts', './expect.ts']; + +if (!process.env.RUN_AGAINST_LOCAL_SERVICES) { + setupFiles.unshift('dotenv/config'); +} else { + setupFiles.unshift('./local-dev.ts'); +} + export default defineConfig({ test: { globals: true, @@ -11,7 +19,7 @@ export default defineConfig({ import.meta.url, ).pathname, }, - setupFiles: ['dotenv/config', '../scripts/serializer.ts', './expect.ts'], + setupFiles, testTimeout: 90_000, }, }); diff --git a/package.json b/package.json index a279d8cc0..d795ee45f 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "build:services": "pnpm prebuild && pnpm turbo build --filter=./packages/services/**/* --filter=./packages/migrations --color", "build:web": "pnpm prebuild && pnpm turbo build --filter=./packages/web/* --color", "cargo:fix": "bash ./scripts/fix-symbolic-link.sh", + "dev:integration": "cd integration-tests && pnpm dev:integration", "docker:build": "docker buildx bake -f docker/docker.hcl --load build", "docker:override-up": "docker compose -f ./docker/docker-compose.override.yml up -d --remove-orphans", "env:sync": "tsx scripts/sync-env-files.ts", diff --git a/packages/libraries/apollo/src/version.ts b/packages/libraries/apollo/src/version.ts index 79b15b8e1..cecdb563d 100644 --- a/packages/libraries/apollo/src/version.ts +++ b/packages/libraries/apollo/src/version.ts @@ -1 +1 @@ -export const version = '0.33.3'; +export const version = '0.33.4'; diff --git a/packages/libraries/cli/package.json b/packages/libraries/cli/package.json index 8e70d6706..486b176a8 100644 --- a/packages/libraries/cli/package.json +++ b/packages/libraries/cli/package.json @@ -71,7 +71,8 @@ "graphql": "16.9.0", "oclif": "4.13.6", "rimraf": "4.4.1", - "tsx": "4.16.2" + "tsx": "4.16.2", + "typescript": "5.5.3" }, "publishConfig": { "registry": "https://registry.npmjs.org", diff --git a/packages/libraries/core/src/version.ts b/packages/libraries/core/src/version.ts index 038b22ba2..2af12a2cc 100644 --- a/packages/libraries/core/src/version.ts +++ b/packages/libraries/core/src/version.ts @@ -1 +1 @@ -export const version = '0.4.0'; +export const version = '0.5.0'; diff --git a/packages/libraries/envelop/src/version.ts b/packages/libraries/envelop/src/version.ts index fcde66c9d..79b15b8e1 100644 --- a/packages/libraries/envelop/src/version.ts +++ b/packages/libraries/envelop/src/version.ts @@ -1 +1 @@ -export const version = '0.33.2'; +export const version = '0.33.3'; diff --git a/packages/libraries/external-composition/package.json b/packages/libraries/external-composition/package.json index c9fcacfb2..fe28a9139 100644 --- a/packages/libraries/external-composition/package.json +++ b/packages/libraries/external-composition/package.json @@ -53,7 +53,8 @@ "typings": "dist/typings/index.d.ts", "scripts": { "build": "bob build && node build-example.mjs", - "check:build": "bob check" + "check:build": "bob check", + "example": "pnpm build && PORT=3012 SECRET=secretsecret node dist/example.js" }, "peerDependencies": { "graphql": "^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" diff --git a/packages/libraries/yoga/src/version.ts b/packages/libraries/yoga/src/version.ts index fcde66c9d..79b15b8e1 100644 --- a/packages/libraries/yoga/src/version.ts +++ b/packages/libraries/yoga/src/version.ts @@ -1 +1 @@ -export const version = '0.33.2'; +export const version = '0.33.3'; diff --git a/packages/services/external-composition/federation-2/.env.template b/packages/services/external-composition/federation-2/.env.template new file mode 100644 index 000000000..e243763e8 --- /dev/null +++ b/packages/services/external-composition/federation-2/.env.template @@ -0,0 +1 @@ +SECRET=secretsecret \ No newline at end of file diff --git a/packages/services/external-composition/federation-2/package.json b/packages/services/external-composition/federation-2/package.json index a7de486ea..5729f6d04 100644 --- a/packages/services/external-composition/federation-2/package.json +++ b/packages/services/external-composition/federation-2/package.json @@ -5,13 +5,14 @@ "private": true, "scripts": { "build": "tsx ../../../../scripts/runify.ts", - "dev": "tsup-node --config ../../../configs/tsup/dev.config.node.ts src/index.ts" + "dev": "tsup-node --config ../../../configs/tsup/dev.config.node.ts src/dev.ts" }, "devDependencies": { "@apollo/composition": "2.8.3", "@apollo/federation-internals": "2.8.3", "@graphql-hive/external-composition": "workspace:*", "@whatwg-node/server": "0.9.36", + "dotenv": "16.4.5", "graphql": "16.9.0", "lru-cache": "^7.17.0", "zod": "3.23.8" diff --git a/packages/services/external-composition/federation-2/src/dev.ts b/packages/services/external-composition/federation-2/src/dev.ts new file mode 100644 index 000000000..bec25773f --- /dev/null +++ b/packages/services/external-composition/federation-2/src/dev.ts @@ -0,0 +1,8 @@ +import { config } from 'dotenv'; + +config({ + debug: true, + encoding: 'utf8', +}); + +await import('./index'); diff --git a/packages/services/rate-limit/.env.template b/packages/services/rate-limit/.env.template index b420f7a94..f8bae2314 100644 --- a/packages/services/rate-limit/.env.template +++ b/packages/services/rate-limit/.env.template @@ -7,4 +7,5 @@ POSTGRES_DB=registry USAGE_ESTIMATOR_ENDPOINT=http://localhost:4011 EMAILS_ENDPOINT=http://localhost:6260 WEB_APP_URL=http://localhost:3000 -OPENTELEMETRY_COLLECTOR_ENDPOINT="" \ No newline at end of file +OPENTELEMETRY_COLLECTOR_ENDPOINT="" +LIMIT_CACHE_UPDATE_INTERVAL_MS=2000 \ No newline at end of file diff --git a/packages/services/schema/src/orchestrators.ts b/packages/services/schema/src/orchestrators.ts index 982970327..7a6fabe60 100644 --- a/packages/services/schema/src/orchestrators.ts +++ b/packages/services/schema/src/orchestrators.ts @@ -362,11 +362,14 @@ async function callExternalService( } logger.info( - 'Network error so return failure (status=%s, message=%s)', + 'Network error so return failure (url=%s, status=%s, message=%s)', + input.url, error.response.statusCode, error.message, ); + logger.error(error); + span.setAttribute('error.message', error.message || ''); span.setAttribute('error.type', error.name); diff --git a/packages/services/server/.env.template b/packages/services/server/.env.template index b34137b8f..48d0f0ea8 100644 --- a/packages/services/server/.env.template +++ b/packages/services/server/.env.template @@ -38,6 +38,10 @@ CDN_CF_AUTH_TOKEN=85e20c26c03759603c0f45884824a1c3 CDN_CF_NAMESPACE_ID=33b1e3bbb4a4707d05ea0307cbb55c79 CDN_BASE_URL="http://localhost:4010" +# Dev/Local CDN Configuration +CDN_API=1 +CDN_API_BASE_URL=http://localhost:3001 + # S3 Artifacts Configuration S3_ENDPOINT=http://localhost:9000 S3_ACCESS_KEY_ID="minioadmin" @@ -55,7 +59,7 @@ SUPERTOKENS_CONNECTION_URI=http://localhost:3567 SUPERTOKENS_API_KEY=bubatzbieber6942096420 # Organization level Open ID Connect Authentication -AUTH_ORGANIZATION_OIDC=0 +AUTH_ORGANIZATION_OIDC=1 ## Enable GitHub login AUTH_GITHUB="" @@ -77,4 +81,6 @@ AUTH_OKTA_ENDPOINT="" AUTH_OKTA_CLIENT_ID="" AUTH_OKTA_CLIENT_SECRET="" -OPENTELEMETRY_COLLECTOR_ENDPOINT="" \ No newline at end of file +OPENTELEMETRY_COLLECTOR_ENDPOINT="" + +HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret \ No newline at end of file diff --git a/packages/services/usage-ingestor/.env.template b/packages/services/usage-ingestor/.env.template index 03eba1245..8b659292e 100644 --- a/packages/services/usage-ingestor/.env.template +++ b/packages/services/usage-ingestor/.env.template @@ -8,6 +8,6 @@ CLICKHOUSE_HOST="localhost" CLICKHOUSE_PORT="8123" CLICKHOUSE_USERNAME="test" CLICKHOUSE_PASSWORD="test" -CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS="30000" -CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE="200000000" +CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS="500" +CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE="1000" PORT=4002 \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 757342413..621c8d2d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -446,6 +446,9 @@ importers: tsx: specifier: 4.16.2 version: 4.16.2 + typescript: + specifier: 5.5.3 + version: 5.5.3 packages/libraries/core: dependencies: @@ -928,6 +931,9 @@ importers: '@whatwg-node/server': specifier: 0.9.36 version: 0.9.36 + dotenv: + specifier: 16.4.5 + version: 16.4.5 graphql: specifier: 16.9.0 version: 16.9.0 @@ -3311,22 +3317,10 @@ packages: '@babel/regjsgen@0.8.0': resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - '@babel/runtime@7.20.7': - resolution: {integrity: sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.23.1': resolution: {integrity: sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.24.0': - resolution: {integrity: sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.24.4': - resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.24.7': resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} engines: {node: '>=6.9.0'} @@ -9809,6 +9803,9 @@ packages: domutils@3.0.1: resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -9904,6 +9901,10 @@ packages: resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} engines: {node: '>=0.12'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + env-ci@7.3.0: resolution: {integrity: sha512-L8vK54CSjKB4pwlwx0YaqeBdUSGufaLHl/pEgD+EqnMrYCVUA8HzMjURALSyvOlC57e953yN7KyXS63qDoc3Rg==} engines: {node: '>=12.20'} @@ -11953,8 +11954,8 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - juice@9.0.0: - resolution: {integrity: sha512-s/IwgQ4caZq3bSnQZlKfdGUqJWy9WzTzB12WSPko9G8uK74H8BJEQvX7GLmFAQ6SLFgAppqC/TUYepKZZaV+JA==} + juice@9.1.0: + resolution: {integrity: sha512-odblShmPrUoHUwRuC8EmLji5bPP2MLO1GL+gt4XU3tT2ECmbSrrMjtMQaqg3wgMFP2zvUzdPZGfxc5Trk3Z+fQ==} engines: {node: '>=10.0.0'} hasBin: true @@ -14543,9 +14544,6 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -17056,28 +17054,28 @@ snapshots: '@aws-sdk/util-endpoints': 3.587.0 '@aws-sdk/util-user-agent-browser': 3.577.0 '@aws-sdk/util-user-agent-node': 3.587.0 - '@smithy/config-resolver': 3.0.4 - '@smithy/core': 2.2.4 - '@smithy/fetch-http-handler': 3.2.0 + '@smithy/config-resolver': 3.0.5 + '@smithy/core': 2.2.6 + '@smithy/fetch-http-handler': 3.2.1 '@smithy/hash-node': 3.0.3 '@smithy/invalid-dependency': 3.0.3 '@smithy/middleware-content-length': 3.0.3 - '@smithy/middleware-endpoint': 3.0.4 - '@smithy/middleware-retry': 3.0.7 + '@smithy/middleware-endpoint': 3.0.5 + '@smithy/middleware-retry': 3.0.9 '@smithy/middleware-serde': 3.0.3 '@smithy/middleware-stack': 3.0.3 - '@smithy/node-config-provider': 3.1.3 - '@smithy/node-http-handler': 3.1.1 + '@smithy/node-config-provider': 3.1.4 + '@smithy/node-http-handler': 3.1.2 '@smithy/protocol-http': 4.0.3 - '@smithy/smithy-client': 3.1.5 + '@smithy/smithy-client': 3.1.7 '@smithy/types': 3.3.0 '@smithy/url-parser': 3.0.3 '@smithy/util-base64': 3.0.0 '@smithy/util-body-length-browser': 3.0.0 '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.7 - '@smithy/util-defaults-mode-node': 3.0.7 - '@smithy/util-endpoints': 2.0.4 + '@smithy/util-defaults-mode-browser': 3.0.9 + '@smithy/util-defaults-mode-node': 3.0.9 + '@smithy/util-endpoints': 2.0.5 '@smithy/util-middleware': 3.0.3 '@smithy/util-retry': 3.0.3 '@smithy/util-utf8': 3.0.0 @@ -17232,28 +17230,28 @@ snapshots: '@aws-sdk/util-endpoints': 3.587.0 '@aws-sdk/util-user-agent-browser': 3.577.0 '@aws-sdk/util-user-agent-node': 3.587.0 - '@smithy/config-resolver': 3.0.4 - '@smithy/core': 2.2.4 - '@smithy/fetch-http-handler': 3.2.0 + '@smithy/config-resolver': 3.0.5 + '@smithy/core': 2.2.6 + '@smithy/fetch-http-handler': 3.2.1 '@smithy/hash-node': 3.0.3 '@smithy/invalid-dependency': 3.0.3 '@smithy/middleware-content-length': 3.0.3 - '@smithy/middleware-endpoint': 3.0.4 - '@smithy/middleware-retry': 3.0.7 + '@smithy/middleware-endpoint': 3.0.5 + '@smithy/middleware-retry': 3.0.9 '@smithy/middleware-serde': 3.0.3 '@smithy/middleware-stack': 3.0.3 - '@smithy/node-config-provider': 3.1.3 - '@smithy/node-http-handler': 3.1.1 + '@smithy/node-config-provider': 3.1.4 + '@smithy/node-http-handler': 3.1.2 '@smithy/protocol-http': 4.0.3 - '@smithy/smithy-client': 3.1.5 + '@smithy/smithy-client': 3.1.7 '@smithy/types': 3.3.0 '@smithy/url-parser': 3.0.3 '@smithy/util-base64': 3.0.0 '@smithy/util-body-length-browser': 3.0.0 '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.7 - '@smithy/util-defaults-mode-node': 3.0.7 - '@smithy/util-endpoints': 2.0.4 + '@smithy/util-defaults-mode-browser': 3.0.9 + '@smithy/util-defaults-mode-node': 3.0.9 + '@smithy/util-endpoints': 2.0.5 '@smithy/util-middleware': 3.0.3 '@smithy/util-retry': 3.0.3 '@smithy/util-utf8': 3.0.0 @@ -18766,22 +18764,10 @@ snapshots: '@babel/regjsgen@0.8.0': {} - '@babel/runtime@7.20.7': - dependencies: - regenerator-runtime: 0.13.11 - '@babel/runtime@7.23.1': dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.24.0': - dependencies: - regenerator-runtime: 0.14.1 - - '@babel/runtime@7.24.4': - dependencies: - regenerator-runtime: 0.14.1 - '@babel/runtime@7.24.7': dependencies: regenerator-runtime: 0.14.1 @@ -19182,7 +19168,7 @@ snapshots: '@emotion/react@11.10.5(@babel/core@7.24.7)(@types/react@18.3.3)(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.7 '@emotion/babel-plugin': 11.10.5(@babel/core@7.24.7) '@emotion/cache': 11.10.5 '@emotion/serialize': 1.1.1 @@ -22699,7 +22685,7 @@ snapshots: '@radix-ui/react-dialog@1.0.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.7 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) '@radix-ui/react-context': 1.0.0(react@18.3.1) @@ -26237,7 +26223,7 @@ snapshots: css-what: 6.1.0 domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.0.1 + domutils: 3.1.0 cheerio@1.0.0-rc.10: dependencies: @@ -26254,7 +26240,7 @@ snapshots: cheerio-select: 2.1.0 dom-serializer: 2.0.0 domhandler: 5.0.3 - domutils: 3.0.1 + domutils: 3.1.0 htmlparser2: 8.0.2 parse5: 7.1.2 parse5-htmlparser2-tree-adapter: 7.0.0 @@ -26952,7 +26938,7 @@ snapshots: date-fns@2.30.0: dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.7 date-fns@3.6.0: {} @@ -27196,6 +27182,12 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -27283,6 +27275,8 @@ snapshots: entities@4.4.0: {} + entities@4.5.0: {} + env-ci@7.3.0: dependencies: execa: 5.1.1 @@ -29175,8 +29169,8 @@ snapshots: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.0.1 - entities: 4.4.0 + domutils: 3.1.0 + entities: 4.5.0 http-cache-semantics@4.1.1: {} @@ -29913,7 +29907,7 @@ snapshots: object.assign: 4.1.5 object.values: 1.1.7 - juice@9.0.0(encoding@0.1.13): + juice@9.1.0(encoding@0.1.13): dependencies: cheerio: 1.0.0-rc.12 commander: 6.2.1 @@ -31217,7 +31211,7 @@ snapshots: mjml-cli@4.14.0(encoding@0.1.13): dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.7 chokidar: 3.5.3 glob: 7.2.3 html-minifier: 4.0.0 @@ -31241,12 +31235,12 @@ snapshots: mjml-core@4.14.0(patch_hash=zxxsxbqejjmcwuzpigutzzq6wa)(encoding@0.1.13): dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.7 cheerio: 1.0.0-rc.10 detect-node: 2.0.4 html-minifier: 4.0.0 js-beautify: 1.14.6 - juice: 9.0.0(encoding@0.1.13) + juice: 9.1.0(encoding@0.1.13) lodash: 4.17.21 mjml-migrate: 4.14.0(encoding@0.1.13) mjml-parser-xml: 4.14.0 @@ -31352,7 +31346,7 @@ snapshots: mjml-migrate@4.14.0(encoding@0.1.13): dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.7 js-beautify: 1.14.6 lodash: 4.17.21 mjml-core: 4.14.0(patch_hash=zxxsxbqejjmcwuzpigutzzq6wa)(encoding@0.1.13) @@ -31378,7 +31372,7 @@ snapshots: mjml-preset-core@4.14.0(encoding@0.1.13): dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.7 mjml-accordion: 4.14.0(encoding@0.1.13) mjml-body: 4.14.0(encoding@0.1.13) mjml-button: 4.14.0(encoding@0.1.13) @@ -31457,7 +31451,7 @@ snapshots: mjml-validator@4.13.0: dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.7 mjml-wrapper@4.14.0(encoding@0.1.13): dependencies: @@ -31470,7 +31464,7 @@ snapshots: mjml@4.14.0(encoding@0.1.13): dependencies: - '@babel/runtime': 7.20.7 + '@babel/runtime': 7.24.7 mjml-cli: 4.14.0(encoding@0.1.13) mjml-core: 4.14.0(patch_hash=zxxsxbqejjmcwuzpigutzzq6wa)(encoding@0.1.13) mjml-migrate: 4.14.0(encoding@0.1.13) @@ -33077,7 +33071,7 @@ snapshots: react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.7 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -33229,8 +33223,6 @@ snapshots: regenerate@1.4.2: {} - regenerator-runtime@0.13.11: {} - regenerator-runtime@0.14.1: {} regenerator-transform@0.15.2: @@ -35579,7 +35571,7 @@ snapshots: yup@0.29.3: dependencies: - '@babel/runtime': 7.24.4 + '@babel/runtime': 7.24.7 fn-name: 3.0.0 lodash: 4.17.21 lodash-es: 4.17.21 diff --git a/scripts/serializers/cli-output.ts b/scripts/serializers/cli-output.ts index 5edf85265..4b77a4491 100644 --- a/scripts/serializers/cli-output.ts +++ b/scripts/serializers/cli-output.ts @@ -9,7 +9,7 @@ export function normalizeCliOutput(value: string) { .replace(/\x1B[[(?);]{0,2}(;?\d)*./g, '') .replace( /(http:\/\/localhost:\d+)\/([^\/]+)\/([^\/]+)\/([^\/]+)/, - '$1/$organization/$project/$target', + '$appUrl/$organization/$project/$target', ) .replace(/history\/[$]*\w+-\w+-\w+-\w+-\w+/i, 'history/$version') .trim(),