Validate range version app dev sync (#22625)
# Introduction Also now validating the workspace version when running a sync manifest <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22625?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+1
@@ -15,6 +15,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
|
||||
packageJsonChecksum: '[checksum]',
|
||||
yarnLockChecksum: '[checksum]',
|
||||
requiredServerVersionRange: null,
|
||||
},
|
||||
permissionFlags: [],
|
||||
skills: [],
|
||||
|
||||
+1
@@ -233,6 +233,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde',
|
||||
requiredServerVersionRange: null,
|
||||
},
|
||||
frontComponents: [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ import { fromRoleConfigToRoleManifest } from '@/cli/utilities/build/manifest/uti
|
||||
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
|
||||
import { validateConditionalAvailabilityUsage } from '@/cli/utilities/build/manifest/utils/validate-conditional-availability-usage';
|
||||
import { validateViewFilterOperands } from '@/cli/utilities/build/manifest/utils/validate-view-filter-operands';
|
||||
import { getEngineVersionRange } from '@/cli/utilities/version/get-engine-version-range';
|
||||
import { type ApplicationConfig, type LogicFunctionConfig } from '@/sdk/define';
|
||||
import { type CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config';
|
||||
import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config';
|
||||
@@ -577,6 +578,7 @@ export const buildManifest = async (
|
||||
aboutDescription: readmeContent,
|
||||
yarnLockChecksum: null,
|
||||
packageJsonChecksum: null,
|
||||
requiredServerVersionRange: getEngineVersionRange(appPath),
|
||||
...(postInstallLogicFunctions.length >= 1
|
||||
? { postInstallLogicFunction: postInstallLogicFunctions[0] }
|
||||
: {}),
|
||||
|
||||
@@ -4,6 +4,7 @@ export type ApplicationConfig = Omit<
|
||||
ApplicationManifest,
|
||||
| 'packageJsonChecksum'
|
||||
| 'yarnLockChecksum'
|
||||
| 'requiredServerVersionRange'
|
||||
| 'postInstallLogicFunction'
|
||||
| 'preInstallLogicFunction'
|
||||
| 'defaultRoleUniversalIdentifier'
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { ApplicationManifestModule } from 'src/engine/core-modules/application/application-manifest/application-manifest.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { ApplicationPackageModule } from 'src/engine/core-modules/application/application-package/application-package.module';
|
||||
import { ApplicationDevelopmentResolver } from 'src/engine/core-modules/application/application-development/application-development.resolver';
|
||||
import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
@@ -16,6 +17,7 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
ApplicationManifestModule,
|
||||
ApplicationPackageModule,
|
||||
ApplicationRegistrationModule,
|
||||
CacheLockModule,
|
||||
FeatureFlagModule,
|
||||
|
||||
+19
@@ -22,6 +22,8 @@ import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/appli
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
|
||||
import { ApplicationVersionValidationService } from 'src/engine/core-modules/application/application-package/application-version-validation.service';
|
||||
import { VERSION_REASON_TO_APPLICATION_EXCEPTION_CODE } from 'src/engine/core-modules/application/application-package/constants/version-reason-to-exception-code.constant';
|
||||
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
@@ -64,6 +66,7 @@ export class ApplicationDevelopmentResolver {
|
||||
private readonly applicationSyncService: ApplicationSyncService,
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
private readonly applicationVersionValidationService: ApplicationVersionValidationService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly sdkClientGenerationService: SdkClientGenerationService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
@@ -118,6 +121,22 @@ export class ApplicationDevelopmentResolver {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const versionValidation =
|
||||
await this.applicationVersionValidationService.validateWorkspaceCompatibility(
|
||||
{
|
||||
requiredServerVersion:
|
||||
manifest.application.requiredServerVersionRange ?? undefined,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (!versionValidation.compatible) {
|
||||
throw new ApplicationException(
|
||||
versionValidation.message,
|
||||
VERSION_REASON_TO_APPLICATION_EXCEPTION_CODE[versionValidation.reason],
|
||||
);
|
||||
}
|
||||
|
||||
if (dryRun === true) {
|
||||
const { workspaceMigration } =
|
||||
await this.applicationSyncService.synchronizeFromManifest({
|
||||
|
||||
+3
-21
@@ -21,10 +21,8 @@ import { ManifestAssetUrlResolverService } from 'src/engine/core-modules/applica
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationPackageFetcherService } from 'src/engine/core-modules/application/application-package/application-package-fetcher.service';
|
||||
import {
|
||||
ApplicationVersionValidationService,
|
||||
type VersionValidationFailureReason,
|
||||
} from 'src/engine/core-modules/application/application-package/application-version-validation.service';
|
||||
import { ApplicationVersionValidationService } from 'src/engine/core-modules/application/application-package/application-version-validation.service';
|
||||
import { VERSION_REASON_TO_APPLICATION_EXCEPTION_CODE } from 'src/engine/core-modules/application/application-package/constants/version-reason-to-exception-code.constant';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/services/file-storage.service';
|
||||
@@ -43,20 +41,6 @@ import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-func
|
||||
export class ApplicationInstallService {
|
||||
private readonly logger = new Logger(ApplicationInstallService.name);
|
||||
|
||||
private static readonly VERSION_REASON_TO_EXCEPTION_CODE: Record<
|
||||
VersionValidationFailureReason,
|
||||
ApplicationExceptionCode
|
||||
> = {
|
||||
INVALID_REQUIRED_VERSION:
|
||||
ApplicationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT,
|
||||
INVALID_SERVER_VERSION: ApplicationExceptionCode.INVALID_SERVER_VERSION,
|
||||
INVALID_WORKSPACE_VERSION:
|
||||
ApplicationExceptionCode.INVALID_WORKSPACE_VERSION,
|
||||
INSTANCE_INCOMPATIBLE: ApplicationExceptionCode.SERVER_VERSION_INCOMPATIBLE,
|
||||
WORKSPACE_INCOMPATIBLE:
|
||||
ApplicationExceptionCode.WORKSPACE_VERSION_INCOMPATIBLE,
|
||||
};
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ApplicationRegistrationEntity)
|
||||
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
@@ -157,9 +141,7 @@ export class ApplicationInstallService {
|
||||
|
||||
throw new ApplicationException(
|
||||
versionValidation.message,
|
||||
ApplicationInstallService.VERSION_REASON_TO_EXCEPTION_CODE[
|
||||
versionValidation.reason
|
||||
],
|
||||
VERSION_REASON_TO_APPLICATION_EXCEPTION_CODE[versionValidation.reason],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { type VersionValidationFailureReason } from 'src/engine/core-modules/application/application-package/application-version-validation.service';
|
||||
import { ApplicationRegistrationExceptionCode } from 'src/engine/core-modules/application/application-registration/application-registration.exception';
|
||||
import { ApplicationExceptionCode } from 'src/engine/core-modules/application/application.exception';
|
||||
|
||||
export const VERSION_REASON_TO_APPLICATION_EXCEPTION_CODE: Record<
|
||||
VersionValidationFailureReason,
|
||||
ApplicationExceptionCode
|
||||
> = {
|
||||
INVALID_REQUIRED_VERSION:
|
||||
ApplicationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT,
|
||||
INVALID_SERVER_VERSION: ApplicationExceptionCode.INVALID_SERVER_VERSION,
|
||||
INVALID_WORKSPACE_VERSION: ApplicationExceptionCode.INVALID_WORKSPACE_VERSION,
|
||||
INSTANCE_INCOMPATIBLE: ApplicationExceptionCode.SERVER_VERSION_INCOMPATIBLE,
|
||||
WORKSPACE_INCOMPATIBLE:
|
||||
ApplicationExceptionCode.WORKSPACE_VERSION_INCOMPATIBLE,
|
||||
};
|
||||
|
||||
// The registration flow (tarball upload) has no per-workspace context, so
|
||||
// workspace-scoped reasons collapse onto the server-scoped registration codes.
|
||||
export const VERSION_REASON_TO_APPLICATION_REGISTRATION_EXCEPTION_CODE: Record<
|
||||
VersionValidationFailureReason,
|
||||
ApplicationRegistrationExceptionCode
|
||||
> = {
|
||||
INVALID_REQUIRED_VERSION:
|
||||
ApplicationRegistrationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT,
|
||||
INVALID_SERVER_VERSION:
|
||||
ApplicationRegistrationExceptionCode.INVALID_SERVER_VERSION,
|
||||
INVALID_WORKSPACE_VERSION:
|
||||
ApplicationRegistrationExceptionCode.INVALID_SERVER_VERSION,
|
||||
INSTANCE_INCOMPATIBLE:
|
||||
ApplicationRegistrationExceptionCode.SERVER_VERSION_INCOMPATIBLE,
|
||||
WORKSPACE_INCOMPATIBLE:
|
||||
ApplicationRegistrationExceptionCode.SERVER_VERSION_INCOMPATIBLE,
|
||||
};
|
||||
+3
-21
@@ -12,10 +12,8 @@ import { Repository } from 'typeorm';
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
ApplicationVersionValidationService,
|
||||
type VersionValidationFailureReason,
|
||||
} from 'src/engine/core-modules/application/application-package/application-version-validation.service';
|
||||
import { ApplicationVersionValidationService } from 'src/engine/core-modules/application/application-package/application-version-validation.service';
|
||||
import { VERSION_REASON_TO_APPLICATION_REGISTRATION_EXCEPTION_CODE } from 'src/engine/core-modules/application/application-package/constants/version-reason-to-exception-code.constant';
|
||||
import { extractTarballSecurely } from 'src/engine/core-modules/application/application-package/utils/extract-tarball-securely.util';
|
||||
import { readJsonFile } from 'src/engine/core-modules/application/application-package/utils/read-json-file.util';
|
||||
import { resolvePackageContentDir } from 'src/engine/core-modules/application/application-package/utils/tarball-utils';
|
||||
@@ -35,22 +33,6 @@ import type { ApplicationManifest } from 'twenty-shared/application';
|
||||
export class ApplicationTarballService {
|
||||
private readonly logger = new Logger(ApplicationTarballService.name);
|
||||
|
||||
private static readonly VERSION_REASON_TO_EXCEPTION_CODE: Record<
|
||||
VersionValidationFailureReason,
|
||||
ApplicationRegistrationExceptionCode
|
||||
> = {
|
||||
INVALID_REQUIRED_VERSION:
|
||||
ApplicationRegistrationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT,
|
||||
INVALID_SERVER_VERSION:
|
||||
ApplicationRegistrationExceptionCode.INVALID_SERVER_VERSION,
|
||||
INVALID_WORKSPACE_VERSION:
|
||||
ApplicationRegistrationExceptionCode.INVALID_SERVER_VERSION,
|
||||
INSTANCE_INCOMPATIBLE:
|
||||
ApplicationRegistrationExceptionCode.SERVER_VERSION_INCOMPATIBLE,
|
||||
WORKSPACE_INCOMPATIBLE:
|
||||
ApplicationRegistrationExceptionCode.SERVER_VERSION_INCOMPATIBLE,
|
||||
};
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ApplicationRegistrationEntity)
|
||||
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
@@ -107,7 +89,7 @@ export class ApplicationTarballService {
|
||||
if (!versionValidation.compatible) {
|
||||
throw new ApplicationRegistrationException(
|
||||
versionValidation.message,
|
||||
ApplicationTarballService.VERSION_REASON_TO_EXCEPTION_CODE[
|
||||
VERSION_REASON_TO_APPLICATION_REGISTRATION_EXCEPTION_CODE[
|
||||
versionValidation.reason
|
||||
],
|
||||
);
|
||||
|
||||
+10
-2
@@ -1,17 +1,25 @@
|
||||
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
export const expectOneNotInternalServerErrorSnapshot = ({
|
||||
errors,
|
||||
normalizeMessage,
|
||||
}: {
|
||||
errors: BaseGraphQLError[];
|
||||
normalizeMessage?: (message: string) => string;
|
||||
}) => {
|
||||
expect(errors.length).toBe(1);
|
||||
const [firstError] = errors;
|
||||
|
||||
expect(firstError.extensions.code).not.toBe('INTERNAL_SERVER_ERROR');
|
||||
expect(firstError).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny(firstError),
|
||||
|
||||
const errorForSnapshot = isDefined(normalizeMessage)
|
||||
? { ...firstError, message: normalizeMessage(firstError.message) }
|
||||
: firstError;
|
||||
|
||||
expect(errorForSnapshot).toMatchSnapshot(
|
||||
extractRecordIdsAndDatesAsExpectAny(errorForSnapshot),
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ exports[`Install application is gated by the workspace completed upgrade version
|
||||
"subCode": "WORKSPACE_VERSION_INCOMPATIBLE",
|
||||
"userFriendlyMessage": "This app requires a newer version than this workspace has finished upgrading to. Please try again once the workspace upgrade completes.",
|
||||
},
|
||||
"message": "App requires Twenty server >=2.20.0 but this workspace has only completed the upgrade to 2.19.0.",
|
||||
"message": "App requires Twenty server >=<version> but this workspace has only completed the upgrade to <version>.",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Sync application is gated by the workspace completed upgrade version rejects sync when the workspace has not completed the required upgrade version 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "WORKSPACE_VERSION_INCOMPATIBLE",
|
||||
"userFriendlyMessage": "This app requires a newer version than this workspace has finished upgrading to. Please try again once the workspace upgrade completes.",
|
||||
},
|
||||
"message": "App requires Twenty server >=<version> but this workspace has only completed the upgrade to <version>.",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application is gated by the workspace completed upgrade version rejects sync when the workspace upgrade cursor cannot be interpreted 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_WORKSPACE_VERSION",
|
||||
"userFriendlyMessage": "This workspace's upgrade state could not be determined. Please try again once the workspace has finished upgrading.",
|
||||
},
|
||||
"message": "Cannot determine the completed upgrade version for workspace 20202020-1c25-4d02-bf25-6aeccf7ea419: no interpretable upgrade cursor found.",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
+9
-2
@@ -4,6 +4,7 @@ import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/
|
||||
import { createAppTarball } from 'test/integration/metadata/suites/application/utils/create-app-tarball.util';
|
||||
import { installApplication } from 'test/integration/metadata/suites/application/utils/install-application.util';
|
||||
import { uploadAppTarball } from 'test/integration/metadata/suites/application/utils/upload-app-tarball.util';
|
||||
import { scrubSemverVersions } from 'test/utils/scrub-semver-versions.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -129,7 +130,10 @@ describe('Install application is gated by the workspace completed upgrade versio
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
normalizeMessage: scrubSemverVersions,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects installation when the workspace upgrade cursor cannot be interpreted', async () => {
|
||||
@@ -156,6 +160,9 @@ describe('Install application is gated by the workspace completed upgrade versio
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
normalizeMessage: scrubSemverVersions,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { scrubSemverVersions } from 'test/utils/scrub-semver-versions.util';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { TWENTY_CURRENT_VERSION } from 'src/engine/core-modules/upgrade/constants/twenty-current-version.constant';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
// The sync flow can run cache-lock retries with real delays, so fake timers
|
||||
// would hang it — mirror the other application suites.
|
||||
jest.setTimeout(120000);
|
||||
|
||||
const INJECTED_CURSOR_MARKER = 'integration-test-sync-workspace-version-gate';
|
||||
|
||||
const injectWorkspaceCursor = async (
|
||||
name: string,
|
||||
status: 'completed' | 'failed',
|
||||
): Promise<void> => {
|
||||
// Inject as attempt 2 with a fresh createdAt (now): this stays unique against
|
||||
// the seeded completed attempt-1 cursor (which may share this name) and
|
||||
// outranks it, so getWorkspaceCompletedVersion reads this attempt.
|
||||
await global.testDataSource.query(
|
||||
`INSERT INTO core."upgradeMigration"
|
||||
(name, status, attempt, "executedByVersion", "workspaceId", "isInitial")
|
||||
VALUES ($1, $2, 2, $3, $4, false)`,
|
||||
[name, status, INJECTED_CURSOR_MARKER, SEED_APPLE_WORKSPACE_ID],
|
||||
);
|
||||
};
|
||||
|
||||
const clearInjectedWorkspaceCursors = async (): Promise<void> => {
|
||||
await global.testDataSource.query(
|
||||
`DELETE FROM core."upgradeMigration"
|
||||
WHERE "workspaceId" = $1 AND "executedByVersion" = $2`,
|
||||
[SEED_APPLE_WORKSPACE_ID, INJECTED_CURSOR_MARKER],
|
||||
);
|
||||
};
|
||||
|
||||
const buildManifestWithRequiredServerVersionRange = ({
|
||||
appId,
|
||||
roleId,
|
||||
requiredServerVersionRange,
|
||||
}: {
|
||||
appId: string;
|
||||
roleId: string;
|
||||
requiredServerVersionRange: string;
|
||||
}): Manifest => {
|
||||
const manifest = buildBaseManifest({ appId, roleId });
|
||||
|
||||
manifest.application.requiredServerVersionRange = requiredServerVersionRange;
|
||||
|
||||
return manifest;
|
||||
};
|
||||
|
||||
describe('Sync application is gated by the workspace completed upgrade version', () => {
|
||||
let currentVersionCommandName: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
// The seeded workspace's cursor is the last step of the current version.
|
||||
// Re-injecting it as a failed attempt makes the workspace resolve to the
|
||||
// previous completed version — i.e. behind the instance.
|
||||
const [workspaceCursor] = await global.testDataSource.query(
|
||||
`SELECT name FROM core."upgradeMigration"
|
||||
WHERE "workspaceId" = $1
|
||||
ORDER BY "createdAt" DESC, attempt DESC
|
||||
LIMIT 1`,
|
||||
[SEED_APPLE_WORKSPACE_ID],
|
||||
);
|
||||
|
||||
if (!isDefined(workspaceCursor)) {
|
||||
throw new Error(
|
||||
`Expected a seeded upgrade cursor for workspace ${SEED_APPLE_WORKSPACE_ID}`,
|
||||
);
|
||||
}
|
||||
|
||||
currentVersionCommandName = workspaceCursor.name;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearInjectedWorkspaceCursors();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('rejects sync when the workspace has not completed the required upgrade version', async () => {
|
||||
const manifest = buildManifestWithRequiredServerVersionRange({
|
||||
appId: uuidv4(),
|
||||
roleId: uuidv4(),
|
||||
requiredServerVersionRange: `>=${TWENTY_CURRENT_VERSION}`,
|
||||
});
|
||||
|
||||
// The workspace failed mid-way through the current version's upgrade
|
||||
// segment, so its last completed version is the previous one.
|
||||
await injectWorkspaceCursor(currentVersionCommandName, 'failed');
|
||||
|
||||
const { errors } = await syncApplication({
|
||||
manifest,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
normalizeMessage: scrubSemverVersions,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects sync when the workspace upgrade cursor cannot be interpreted', async () => {
|
||||
const manifest = buildManifestWithRequiredServerVersionRange({
|
||||
appId: uuidv4(),
|
||||
roleId: uuidv4(),
|
||||
requiredServerVersionRange: '>=1.0.0',
|
||||
});
|
||||
|
||||
// A cursor pointing at a command outside the supported upgrade sequence
|
||||
// cannot be mapped to a completed version.
|
||||
await injectWorkspaceCursor(
|
||||
'1.0.0_UnknownLegacyCommand_1700000000000',
|
||||
'completed',
|
||||
);
|
||||
|
||||
const { errors } = await syncApplication({
|
||||
manifest,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
normalizeMessage: scrubSemverVersions,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const scrubSemverVersions = (value: string): string =>
|
||||
value.replace(/\d+\.\d+\.\d+/g, '<version>');
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type PostInstallLogicFunctionApplicationManifest } from '@/application/postInstallLogicFunctionApplicationType';
|
||||
import { type PreInstallLogicFunctionApplicationManifest } from '@/application/preInstallLogicFunctionApplicationType';
|
||||
import { type ApplicationCategory } from './applicationCategoryType';
|
||||
import { type ApplicationVariables } from './applicationVariablesType';
|
||||
import { type ServerVariables } from './server-variables.type';
|
||||
import { type SyncableEntityOptions } from './syncableEntityOptionsType';
|
||||
import { type PostInstallLogicFunctionApplicationManifest } from '@/application/postInstallLogicFunctionApplicationType';
|
||||
import { type PreInstallLogicFunctionApplicationManifest } from '@/application/preInstallLogicFunctionApplicationType';
|
||||
|
||||
export type ApplicationManifest = SyncableEntityOptions & {
|
||||
defaultRoleUniversalIdentifier: string;
|
||||
@@ -30,4 +30,5 @@ export type ApplicationManifest = SyncableEntityOptions & {
|
||||
settingsCustomTabFrontComponentUniversalIdentifier?: string;
|
||||
packageJsonChecksum: string | null;
|
||||
yarnLockChecksum: string | null;
|
||||
requiredServerVersionRange?: string | null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user