run integration tests locally against local running services (#5096)

This commit is contained in:
Dotan Simha 2024-07-15 14:21:23 +03:00 committed by GitHub
parent bfeddbf6e8
commit 1ca63a758b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1638 additions and 1484 deletions

View file

@ -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"
}
]
}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,25 @@
import { readFileSync } from 'fs';
import { parse } from 'dotenv';
function applyEnv(env: Record<string, string>) {
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,
});

View file

@ -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"

View file

@ -1,10 +1,7 @@
import { getServiceHost } from './utils';
export const serviceName = 'external_composition';
export const servicePort = 3012;
export async function history(): Promise<string[]> {
const dockerAddress = await getServiceHost(serviceName, servicePort);
const dockerAddress = await getServiceHost('external_composition', 3012);
const res = await fetch(`http://${dockerAddress}/_history`, {
method: 'GET',
headers: {

View file

@ -11,7 +11,44 @@ function getDockerConnection() {
return docker;
}
export async function getServiceHost(serviceName: string, servicePort: number): Promise<string> {
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<string> {
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() {

View file

@ -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',

View file

@ -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();

View file

@ -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();

View file

@ -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
},
);

View file

@ -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');
},
);

View file

@ -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();

View file

@ -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();

View file

@ -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);

View file

@ -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);

View file

@ -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({

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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(

View file

@ -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,

View file

@ -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(
{

View file

@ -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);

View file

@ -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);

View file

@ -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")',
);
},
);

File diff suppressed because it is too large Load diff

View file

@ -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
`);
});
});

View file

@ -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`,
),
);
});

View file

@ -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`,
),
);
});

View file

@ -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
`);
});
});

View file

@ -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`,
),
);
});

View file

@ -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
`);
});
});

View file

@ -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`,
),
);
});

View file

@ -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<SchemaBuilderApi>({
links: [

View file

@ -12,5 +12,5 @@
}
]
},
"include": ["./testkit", "./tests", "./expect.ts"]
"include": ["./testkit", "./tests", "./expect.ts", "local-dev.ts"]
}

View file

@ -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,
},
});

View file

@ -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",

View file

@ -1 +1 @@
export const version = '0.33.3';
export const version = '0.33.4';

View file

@ -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",

View file

@ -1 +1 @@
export const version = '0.4.0';
export const version = '0.5.0';

View file

@ -1 +1 @@
export const version = '0.33.2';
export const version = '0.33.3';

View file

@ -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"

View file

@ -1 +1 @@
export const version = '0.33.2';
export const version = '0.33.3';

View file

@ -0,0 +1 @@
SECRET=secretsecret

View file

@ -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"

View file

@ -0,0 +1,8 @@
import { config } from 'dotenv';
config({
debug: true,
encoding: 'utf8',
});
await import('./index');

View file

@ -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="<sync>"
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
LIMIT_CACHE_UPDATE_INTERVAL_MS=2000

View file

@ -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);

View file

@ -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="<sync>"
@ -77,4 +81,6 @@ AUTH_OKTA_ENDPOINT="<sync>"
AUTH_OKTA_CLIENT_ID="<sync>"
AUTH_OKTA_CLIENT_SECRET="<sync>"
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret

View file

@ -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

View file

@ -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

View file

@ -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(),