Files
twenty/packages/twenty-server/test/integration/metadata/suites/application/failing-application-version-progression.integration-spec.ts
T
martmull 6360599943 Unify application version gate, registration writes and upgrade paths across sources (#22931)
Continues the source-unification arc after #22921. Three related
consolidations, one commit each.

## 1. Single semver version gate (`793f4849`)

The "incoming version must move forward" rule was hand-rolled twice:
workspace installs (validate semver, reject equal as
`APP_ALREADY_INSTALLED`, lower as `CANNOT_DOWNGRADE_APPLICATION`) and
tarball deploys (reject `lte` as `VERSION_ALREADY_EXISTS`).
`ApplicationVersionValidationService.validateVersionProgression` now
owns the comparison rules and messages; new maps in
`version-reason-to-exception-code.constant.ts` translate failure reasons
to each caller's existing exception codes, so error contracts observed
by the frontend/CLI are unchanged. A non-semver current version never
blocks, matching both previous behaviors.

## 2. One registration-metadata writer (`13eb5f91`)

Tarball upload and marketplace catalog sync wrote registration metadata
with their own repository calls, duplicating the gallery-image fileId
preservation and variable-schema sync, and bypassing the
per-registration lock and transaction that `updateFromManifest`
provides. Both now delegate to `updateFromManifest` (new
`additionalFields` allowlist for their extra columns: `tarballFileId`,
`isListed`, `isVetted`, `ownerWorkspaceId`, `sourcePackage`, `name`), so
every manifest-bearing registration write serializes on the same lock
and applies the same rules. The shared gallery fileId preservation moved
to a `buildRegistrationManifestUpdateFields` util. Tarball uploads can
no longer race installs on the registration row.

Behavior notes: a tarball re-upload whose manifest lacks
`application.displayName` now keeps the existing registration name
instead of resetting it to "Unknown App", and a re-upload without a
`package.json` version keeps the stored `latestAvailableVersion` instead
of nulling it — both strictly less destructive.

## 3. TARBALL upgrades (`b20aaba9`)

`upgradeApplication` only supported NPM; TARBALL apps had no update path
for installing workspaces. It now accepts TARBALL registrations and
re-installs the stored tarball, whose contents define the target version
— the install flow already gates same-version and downgrade installs.
The settings UI shows the latest-version row and the Upgrade button for
both NPM and TARBALL apps via a shared
`isUpgradableApplicationSourceType` util. LOCAL (dev-sync updates) and
OAUTH_ONLY (no code artifacts) stay rejected with a clearer message.

## Validation

- New tests: `validateVersionProgression` matrix in
`application-version-validation.service.spec.ts`,
`buildRegistrationManifestUpdateFields` gallery-preservation spec
- All 27 application suites (148 tests) pass; typecheck and lint green
on twenty-server and twenty-front
2026-07-17 09:06:20 +02:00

98 lines
3.3 KiB
TypeScript

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 { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
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';
// Fixed identifiers keep the exception messages, and therefore the error
// snapshots, stable across runs.
const APP_UNIVERSAL_IDENTIFIER = '20202020-6b0e-4b1e-9d3a-000000002931';
const ROLE_UNIVERSAL_IDENTIFIER = '20202020-6b0e-4b1e-9d3a-000000002932';
// The upload flow runs cache-lock retries with real delays, so fake timers
// would hang it — mirror the other application suites.
jest.setTimeout(120000);
const buildTarball = (version: string): Promise<Buffer> =>
createAppTarball({
'manifest.json': JSON.stringify(
buildBaseManifest({
appId: APP_UNIVERSAL_IDENTIFIER,
roleId: ROLE_UNIVERSAL_IDENTIFIER,
}),
),
'package.json': JSON.stringify({
name: 'test-version-progression',
version,
}),
});
describe('Application version progression gate', () => {
beforeAll(async () => {
jest.useRealTimers();
const tarball = await buildTarball('1.0.0');
await uploadAppTarball({
tarballBuffer: tarball,
universalIdentifier: APP_UNIVERSAL_IDENTIFIER,
});
});
afterAll(async () => {
await cleanupApplicationAndAppRegistration({
applicationUniversalIdentifier: APP_UNIVERSAL_IDENTIFIER,
});
jest.useFakeTimers();
});
it('rejects re-deploying the currently deployed version', async () => {
const tarball = await buildTarball('1.0.0');
const { errors } = await uploadAppTarball({
tarballBuffer: tarball,
universalIdentifier: APP_UNIVERSAL_IDENTIFIER,
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
normalizeMessage: scrubSemverVersions,
});
});
it('rejects deploying a version lower than the deployed one', async () => {
const tarball = await buildTarball('0.9.0');
const { errors } = await uploadAppTarball({
tarballBuffer: tarball,
universalIdentifier: APP_UNIVERSAL_IDENTIFIER,
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
normalizeMessage: scrubSemverVersions,
});
});
it('rejects re-installing the already installed version', async () => {
await installApplication({
input: { universalIdentifier: APP_UNIVERSAL_IDENTIFIER },
});
const { errors } = await installApplication({
input: { universalIdentifier: APP_UNIVERSAL_IDENTIFIER },
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({
errors,
normalizeMessage: scrubSemverVersions,
});
});
});