6360599943
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
98 lines
3.3 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|