Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
506 lines
15 KiB
TypeScript
506 lines
15 KiB
TypeScript
import { randomUUID } from 'crypto';
|
|
|
|
import { gql } from 'graphql-tag';
|
|
import { type DataSource } from 'typeorm';
|
|
|
|
import { makeAdminPanelAPIRequestWithGuestRole } from 'test/integration/graphql/suites/admin-panel/utils/make-admin-panel-api-request-with-guest-role.util';
|
|
import { makeAdminPanelAPIRequest } from 'test/integration/twenty-config/utils/make-admin-panel-api-request.util';
|
|
|
|
import {
|
|
SEED_APPLE_WORKSPACE_ID,
|
|
SEED_YCOMBINATOR_WORKSPACE_ID,
|
|
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
|
|
|
// displayNames of the seeded workspaces (see seeder-workspaces.constant.ts).
|
|
// They drive the ASC ordering and the searchTerm-by-workspace-name assertions.
|
|
const APPLE_WORKSPACE_DISPLAY_NAME = 'Apple';
|
|
const YCOMBINATOR_WORKSPACE_DISPLAY_NAME = 'YCombinator';
|
|
|
|
const FIND_STATS = gql`
|
|
query FindAdminApplicationRegistrationStats($id: String!) {
|
|
findAdminApplicationRegistrationStats(id: $id) {
|
|
activeInstalls
|
|
suspendedInstalls
|
|
mostInstalledVersion
|
|
versionDistribution {
|
|
version
|
|
count
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const FIND_INSTALLED_WORKSPACES = gql`
|
|
query FindAdminApplicationRegistrationInstalledWorkspaces(
|
|
$input: FindApplicationRegistrationInstalledWorkspacesInput!
|
|
) {
|
|
findAdminApplicationRegistrationInstalledWorkspaces(input: $input) {
|
|
totalCount
|
|
hasMore
|
|
workspaces {
|
|
id
|
|
displayName
|
|
logo
|
|
version
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const FIND_ALL_REGISTRATIONS = gql`
|
|
query FindAllApplicationRegistrations(
|
|
$limit: Int
|
|
$offset: Int
|
|
$searchTerm: String
|
|
$isPreInstalledOnly: Boolean
|
|
) {
|
|
findAllApplicationRegistrations(
|
|
limit: $limit
|
|
offset: $offset
|
|
searchTerm: $searchTerm
|
|
isPreInstalledOnly: $isPreInstalledOnly
|
|
) {
|
|
totalCount
|
|
hasMore
|
|
registrations {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const REGISTRATION_NAME = 'admin-panel-stats-integration-test-registration';
|
|
|
|
const DELETED_WORKSPACE_ONLY_VERSION = '9.9.9';
|
|
|
|
type SeededApplication = {
|
|
id: string;
|
|
workspaceId: string;
|
|
version: string;
|
|
};
|
|
|
|
describe('Admin panel application registration stats and installed workspaces (integration)', () => {
|
|
let dataSource: DataSource;
|
|
let applicationRegistrationId: string;
|
|
let deletedWorkspaceId: string;
|
|
const seededApplicationIds: string[] = [];
|
|
|
|
const insertApplication = async ({
|
|
workspaceId,
|
|
version,
|
|
}: {
|
|
workspaceId: string;
|
|
version: string;
|
|
}): Promise<SeededApplication> => {
|
|
const id = randomUUID();
|
|
|
|
await dataSource.query(
|
|
`INSERT INTO core."application"
|
|
(id, "universalIdentifier", name, version, "sourcePath",
|
|
"sourceType", "workspaceId", "applicationRegistrationId")
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
[
|
|
id,
|
|
randomUUID(),
|
|
'admin-panel-stats-integration-test-app',
|
|
version,
|
|
'',
|
|
'local',
|
|
workspaceId,
|
|
applicationRegistrationId,
|
|
],
|
|
);
|
|
|
|
seededApplicationIds.push(id);
|
|
|
|
return { id, workspaceId, version };
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
dataSource = global.testDataSource;
|
|
|
|
applicationRegistrationId = randomUUID();
|
|
|
|
await dataSource.query(
|
|
`INSERT INTO core."applicationRegistration"
|
|
(id, "universalIdentifier", name, "oAuthClientId",
|
|
"oAuthRedirectUris", "oAuthScopes", "sourceType")
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
[
|
|
applicationRegistrationId,
|
|
randomUUID(),
|
|
REGISTRATION_NAME,
|
|
randomUUID(),
|
|
[],
|
|
[],
|
|
'local',
|
|
],
|
|
);
|
|
|
|
// Two installs on version 2.0.0 (one per seeded workspace) and one on
|
|
// 1.0.0, so the distribution is deterministic and 2.0.0 is the most
|
|
// installed version.
|
|
await insertApplication({
|
|
workspaceId: SEED_APPLE_WORKSPACE_ID,
|
|
version: '2.0.0',
|
|
});
|
|
await insertApplication({
|
|
workspaceId: SEED_YCOMBINATOR_WORKSPACE_ID,
|
|
version: '2.0.0',
|
|
});
|
|
await insertApplication({
|
|
workspaceId: SEED_YCOMBINATOR_WORKSPACE_ID,
|
|
version: '1.0.0',
|
|
});
|
|
|
|
deletedWorkspaceId = randomUUID();
|
|
|
|
const [{ workspaceCustomApplicationId }] = await dataSource.query(
|
|
`SELECT "workspaceCustomApplicationId" FROM core."workspace" WHERE id = $1`,
|
|
[SEED_APPLE_WORKSPACE_ID],
|
|
);
|
|
|
|
await dataSource.query(
|
|
`INSERT INTO core."workspace"
|
|
(id, "displayName", subdomain, "activationStatus",
|
|
"workspaceCustomApplicationId", "deletedAt")
|
|
VALUES ($1, $2, $3, $4, $5, now())`,
|
|
[
|
|
deletedWorkspaceId,
|
|
'admin-panel-stats-deleted-workspace',
|
|
`admin-panel-stats-deleted-${deletedWorkspaceId}`,
|
|
'PENDING_CREATION',
|
|
workspaceCustomApplicationId,
|
|
],
|
|
);
|
|
|
|
await insertApplication({
|
|
workspaceId: deletedWorkspaceId,
|
|
version: DELETED_WORKSPACE_ONLY_VERSION,
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (seededApplicationIds.length > 0) {
|
|
await dataSource.query(
|
|
`DELETE FROM core."application" WHERE id = ANY($1)`,
|
|
[seededApplicationIds],
|
|
);
|
|
}
|
|
|
|
await dataSource.query(
|
|
`DELETE FROM core."applicationRegistration" WHERE id = $1`,
|
|
[applicationRegistrationId],
|
|
);
|
|
|
|
await dataSource.query(`DELETE FROM core."workspace" WHERE id = $1`, [
|
|
deletedWorkspaceId,
|
|
]);
|
|
});
|
|
|
|
describe('findAdminApplicationRegistrationStats', () => {
|
|
it('returns active installs, version distribution and the most installed version', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_STATS,
|
|
variables: { id: applicationRegistrationId },
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const stats = response.body.data?.findAdminApplicationRegistrationStats;
|
|
|
|
expect(stats).toBeDefined();
|
|
expect(stats.activeInstalls).toBe(3);
|
|
expect(stats.suspendedInstalls).toBe(0);
|
|
expect(stats.mostInstalledVersion).toBe('2.0.0');
|
|
|
|
const distributionByVersion = Object.fromEntries(
|
|
stats.versionDistribution.map(
|
|
(entry: { version: string; count: number }) => [
|
|
entry.version,
|
|
entry.count,
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(distributionByVersion).toEqual({ '2.0.0': 2, '1.0.0': 1 });
|
|
// Distribution is ordered by count DESC, so the top entry matches
|
|
// mostInstalledVersion.
|
|
expect(stats.versionDistribution[0].version).toBe('2.0.0');
|
|
});
|
|
|
|
it('excludes installs on soft-deleted workspaces', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_STATS,
|
|
variables: { id: applicationRegistrationId },
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const stats = response.body.data?.findAdminApplicationRegistrationStats;
|
|
|
|
expect(stats.activeInstalls).toBe(3);
|
|
expect(
|
|
stats.versionDistribution.map(
|
|
(entry: { version: string }) => entry.version,
|
|
),
|
|
).not.toContain(DELETED_WORKSPACE_ONLY_VERSION);
|
|
});
|
|
|
|
it('rejects a caller without the SECURITY permission flag', async () => {
|
|
const response = await makeAdminPanelAPIRequestWithGuestRole({
|
|
query: FIND_STATS,
|
|
variables: { id: applicationRegistrationId },
|
|
});
|
|
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(
|
|
response.body.data?.findAdminApplicationRegistrationStats,
|
|
).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('findAdminApplicationRegistrationInstalledWorkspaces', () => {
|
|
it('returns installed workspaces ordered by display name with totalCount and hasMore', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_INSTALLED_WORKSPACES,
|
|
variables: { input: { id: applicationRegistrationId, offset: 0 } },
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const result =
|
|
response.body.data?.findAdminApplicationRegistrationInstalledWorkspaces;
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.totalCount).toBe(3);
|
|
expect(result.hasMore).toBe(false);
|
|
expect(result.workspaces).toHaveLength(3);
|
|
|
|
// Ordered by workspace.displayName ASC — Apple before YCombinator.
|
|
expect(result.workspaces[0].displayName).toBe(
|
|
APPLE_WORKSPACE_DISPLAY_NAME,
|
|
);
|
|
expect(result.workspaces[0].version).toBe('2.0.0');
|
|
expect(result.workspaces[1].displayName).toBe(
|
|
YCOMBINATOR_WORKSPACE_DISPLAY_NAME,
|
|
);
|
|
expect(result.workspaces[2].displayName).toBe(
|
|
YCOMBINATOR_WORKSPACE_DISPLAY_NAME,
|
|
);
|
|
});
|
|
|
|
it('filters by workspace display name via searchTerm', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_INSTALLED_WORKSPACES,
|
|
variables: {
|
|
input: {
|
|
id: applicationRegistrationId,
|
|
offset: 0,
|
|
searchTerm: APPLE_WORKSPACE_DISPLAY_NAME,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const result =
|
|
response.body.data?.findAdminApplicationRegistrationInstalledWorkspaces;
|
|
|
|
expect(result.totalCount).toBe(1);
|
|
expect(result.hasMore).toBe(false);
|
|
expect(result.workspaces).toHaveLength(1);
|
|
expect(result.workspaces[0].displayName).toBe(
|
|
APPLE_WORKSPACE_DISPLAY_NAME,
|
|
);
|
|
});
|
|
|
|
it('returns an empty result instead of erroring when a search only matches installs on soft-deleted workspaces', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_INSTALLED_WORKSPACES,
|
|
variables: {
|
|
input: {
|
|
id: applicationRegistrationId,
|
|
offset: 0,
|
|
searchTerm: DELETED_WORKSPACE_ONLY_VERSION,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const result =
|
|
response.body.data?.findAdminApplicationRegistrationInstalledWorkspaces;
|
|
|
|
expect(result.totalCount).toBe(0);
|
|
expect(result.hasMore).toBe(false);
|
|
expect(result.workspaces).toHaveLength(0);
|
|
});
|
|
|
|
it('filters by application version via searchTerm', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_INSTALLED_WORKSPACES,
|
|
variables: {
|
|
input: {
|
|
id: applicationRegistrationId,
|
|
offset: 0,
|
|
searchTerm: '1.0.0',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const result =
|
|
response.body.data?.findAdminApplicationRegistrationInstalledWorkspaces;
|
|
|
|
expect(result.totalCount).toBe(1);
|
|
expect(result.workspaces).toHaveLength(1);
|
|
expect(result.workspaces[0].version).toBe('1.0.0');
|
|
expect(result.workspaces[0].displayName).toBe(
|
|
YCOMBINATOR_WORKSPACE_DISPLAY_NAME,
|
|
);
|
|
});
|
|
|
|
it('rejects a caller without the SECURITY permission flag', async () => {
|
|
const response = await makeAdminPanelAPIRequestWithGuestRole({
|
|
query: FIND_INSTALLED_WORKSPACES,
|
|
variables: { input: { id: applicationRegistrationId, offset: 0 } },
|
|
});
|
|
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(
|
|
response.body.data?.findAdminApplicationRegistrationInstalledWorkspaces,
|
|
).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('findAllApplicationRegistrations', () => {
|
|
it('filters registrations by name via searchTerm', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_ALL_REGISTRATIONS,
|
|
variables: { limit: 25, offset: 0, searchTerm: REGISTRATION_NAME },
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const result = response.body.data?.findAllApplicationRegistrations;
|
|
|
|
expect(result.totalCount).toBe(1);
|
|
expect(result.hasMore).toBe(false);
|
|
expect(result.registrations).toHaveLength(1);
|
|
expect(result.registrations[0].id).toBe(applicationRegistrationId);
|
|
expect(result.registrations[0].name).toBe(REGISTRATION_NAME);
|
|
});
|
|
|
|
it('returns no registrations when searchTerm matches nothing', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_ALL_REGISTRATIONS,
|
|
variables: {
|
|
limit: 25,
|
|
offset: 0,
|
|
searchTerm: 'no-registration-matches-this-search-term',
|
|
},
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const result = response.body.data?.findAllApplicationRegistrations;
|
|
|
|
expect(result.totalCount).toBe(0);
|
|
expect(result.registrations).toHaveLength(0);
|
|
});
|
|
|
|
it('rejects a caller without the SECURITY permission flag', async () => {
|
|
const response = await makeAdminPanelAPIRequestWithGuestRole({
|
|
query: FIND_ALL_REGISTRATIONS,
|
|
variables: { limit: 25, offset: 0 },
|
|
});
|
|
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(response.body.data?.findAllApplicationRegistrations).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('suspended workspace installs', () => {
|
|
let suspendedWorkspaceId: string;
|
|
let suspendedApplicationId: string;
|
|
|
|
beforeAll(async () => {
|
|
suspendedWorkspaceId = randomUUID();
|
|
|
|
const [{ workspaceCustomApplicationId }] = await dataSource.query(
|
|
`SELECT "workspaceCustomApplicationId" FROM core."workspace" WHERE id = $1`,
|
|
[SEED_APPLE_WORKSPACE_ID],
|
|
);
|
|
|
|
await dataSource.query(
|
|
`INSERT INTO core."workspace"
|
|
(id, "displayName", subdomain, "activationStatus", "defaultRoleId",
|
|
"databaseSchema", "workspaceCustomApplicationId")
|
|
VALUES ($1, $2, $3, 'SUSPENDED', $4, $5, $6)`,
|
|
[
|
|
suspendedWorkspaceId,
|
|
'admin-panel-stats-suspended-workspace',
|
|
`admin-panel-stats-suspended-${suspendedWorkspaceId}`,
|
|
randomUUID(),
|
|
'admin_panel_stats_suspended',
|
|
workspaceCustomApplicationId,
|
|
],
|
|
);
|
|
|
|
const { id } = await insertApplication({
|
|
workspaceId: suspendedWorkspaceId,
|
|
version: '2.0.0',
|
|
});
|
|
|
|
suspendedApplicationId = id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await dataSource.query(`DELETE FROM core."application" WHERE id = $1`, [
|
|
suspendedApplicationId,
|
|
]);
|
|
await dataSource.query(`DELETE FROM core."workspace" WHERE id = $1`, [
|
|
suspendedWorkspaceId,
|
|
]);
|
|
});
|
|
|
|
it('counts them as installs and reports them separately', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_STATS,
|
|
variables: { id: applicationRegistrationId },
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const stats = response.body.data?.findAdminApplicationRegistrationStats;
|
|
|
|
expect(stats.activeInstalls).toBe(4);
|
|
expect(stats.suspendedInstalls).toBe(1);
|
|
});
|
|
|
|
it('lists them alongside installs on active workspaces', async () => {
|
|
const response = await makeAdminPanelAPIRequest({
|
|
query: FIND_INSTALLED_WORKSPACES,
|
|
variables: {
|
|
input: {
|
|
id: applicationRegistrationId,
|
|
offset: 0,
|
|
searchTerm: 'admin-panel-stats-suspended-workspace',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.body.errors).toBeUndefined();
|
|
|
|
const result =
|
|
response.body.data?.findAdminApplicationRegistrationInstalledWorkspaces;
|
|
|
|
expect(result.totalCount).toBe(1);
|
|
expect(result.workspaces[0].id).toBe(suspendedWorkspaceId);
|
|
});
|
|
});
|
|
});
|