From 97f3bc3da560aee28204fbaffc0494a81d84217e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 4 Mar 2026 13:29:28 +0100 Subject: [PATCH] fix: update integration tests and exception filter for refactored types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update app-distribution tests: 'none' sourceType → 'local' - Handle ApplicationRegistrationException in REST API exception filter so tarball upload errors return proper HTTP status codes - Catch manifest.json read failure in tarball upload and throw INVALID_INPUT (400) instead of PACKAGE_RESOLUTION_FAILED (500) - Update marketplace-catalog-sync test to include marketplaceDisplayData in raw SQL inserts (matching what the real sync cron produces) Made-with: Cursor --- .../services/app-tarball-upload.service.ts | 13 ++++- .../application-rest-api-exception-filter.ts | 53 ++++++++++++++++++- .../app-distribution.integration-spec.ts | 8 +-- ...rketplace-catalog-sync.integration-spec.ts | 16 ++++-- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/application-registration/services/app-tarball-upload.service.ts b/packages/twenty-server/src/engine/core-modules/application-registration/services/app-tarball-upload.service.ts index 3c8b63f8c50..f05f73e1f72 100644 --- a/packages/twenty-server/src/engine/core-modules/application-registration/services/app-tarball-upload.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application-registration/services/app-tarball-upload.service.ts @@ -53,12 +53,21 @@ export class AppTarballUploadService { const contentDir = await resolvePackageContentDir(extractDir); - const manifest = await readJsonFile<{ + let manifest: { application?: { universalIdentifier?: string; displayName?: string; }; - }>(contentDir, 'manifest.json'); + }; + + try { + manifest = await readJsonFile(contentDir, 'manifest.json'); + } catch { + throw new ApplicationRegistrationException( + 'manifest.json not found or invalid in tarball', + ApplicationRegistrationExceptionCode.INVALID_INPUT, + ); + } const universalIdentifier = params.universalIdentifier ?? manifest.application?.universalIdentifier; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-rest-api-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/application/application-rest-api-exception-filter.ts index 37389cb6bc8..71bfed8a5cb 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-rest-api-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-rest-api-exception-filter.ts @@ -6,22 +6,40 @@ import { import { type Response } from 'express'; +import { + ApplicationRegistrationException, + ApplicationRegistrationExceptionCode, +} from 'src/engine/core-modules/application-registration/application-registration.exception'; import { ApplicationException, ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service'; -@Catch(ApplicationException) +@Catch(ApplicationException, ApplicationRegistrationException) export class ApplicationRestApiExceptionFilter implements ExceptionFilter { constructor( private readonly httpExceptionHandlerService: HttpExceptionHandlerService, ) {} - catch(exception: ApplicationException, host: ArgumentsHost) { + catch( + exception: ApplicationException | ApplicationRegistrationException, + host: ArgumentsHost, + ) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); + if (exception instanceof ApplicationRegistrationException) { + return this.handleRegistrationException(exception, response); + } + + return this.handleApplicationException(exception, response); + } + + private handleApplicationException( + exception: ApplicationException, + response: Response, + ) { switch (exception.code) { case ApplicationExceptionCode.OBJECT_NOT_FOUND: case ApplicationExceptionCode.FIELD_NOT_FOUND: @@ -60,4 +78,35 @@ export class ApplicationRestApiExceptionFilter implements ExceptionFilter { ); } } + + private handleRegistrationException( + exception: ApplicationRegistrationException, + response: Response, + ) { + switch (exception.code) { + case ApplicationRegistrationExceptionCode.APPLICATION_REGISTRATION_NOT_FOUND: + case ApplicationRegistrationExceptionCode.VARIABLE_NOT_FOUND: + return this.httpExceptionHandlerService.handleError( + exception, + response, + 404, + ); + case ApplicationRegistrationExceptionCode.INVALID_INPUT: + case ApplicationRegistrationExceptionCode.INVALID_SCOPE: + case ApplicationRegistrationExceptionCode.INVALID_REDIRECT_URI: + case ApplicationRegistrationExceptionCode.SOURCE_CHANNEL_MISMATCH: + case ApplicationRegistrationExceptionCode.UNIVERSAL_IDENTIFIER_ALREADY_CLAIMED: + return this.httpExceptionHandlerService.handleError( + exception, + response, + 400, + ); + default: + return this.httpExceptionHandlerService.handleError( + exception, + response, + 500, + ); + } + } } diff --git a/packages/twenty-server/test/integration/metadata/suites/application/app-distribution.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/app-distribution.integration-spec.ts index 503564ac9cb..4a0be31b21e 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/app-distribution.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/app-distribution.integration-spec.ts @@ -75,7 +75,7 @@ const insertRegistrationWithSource = async ( params: { universalIdentifier: string; name: string; - sourceType: 'npm' | 'tarball' | 'none'; + sourceType: 'npm' | 'tarball' | 'local'; sourcePackage?: string; }, ): Promise => { @@ -248,13 +248,13 @@ describe('App Distribution (integration)', () => { expect(res.body.messages[0]).toContain('registered as npm'); }); - it('should allow tarball upload for none-sourced registration', async () => { + it('should allow tarball upload for local-sourced registration', async () => { const uid = crypto.randomUUID(); const regId = await insertRegistrationWithSource(ds, { universalIdentifier: uid, - name: 'None Source App', - sourceType: 'none', + name: 'Local Source App', + sourceType: 'local', }); createdRegistrationIds.push(regId); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/marketplace-catalog-sync.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/marketplace-catalog-sync.integration-spec.ts index 01e4feae166..c92b6b2a663 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/marketplace-catalog-sync.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/marketplace-catalog-sync.integration-spec.ts @@ -58,6 +58,7 @@ describe('Marketplace Catalog Sync (integration)', () => { name: string; sourcePackage: string; latestAvailableVersion?: string; + marketplaceDisplayData?: Record; }): Promise => { const id = crypto.randomUUID(); const oAuthClientId = crypto.randomUUID(); @@ -66,8 +67,9 @@ describe('Marketplace Catalog Sync (integration)', () => { `INSERT INTO core."applicationRegistration" (id, "universalIdentifier", name, "oAuthClientId", "oAuthRedirectUris", "oAuthScopes", "workspaceId", - "sourceType", "sourcePackage", "latestAvailableVersion") - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, + "sourceType", "sourcePackage", "latestAvailableVersion", + "marketplaceDisplayData") + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ id, params.universalIdentifier, @@ -79,6 +81,9 @@ describe('Marketplace Catalog Sync (integration)', () => { 'npm', params.sourcePackage, params.latestAvailableVersion ?? '1.0.0', + params.marketplaceDisplayData + ? JSON.stringify(params.marketplaceDisplayData) + : null, ], ); @@ -141,14 +146,17 @@ describe('Marketplace Catalog Sync (integration)', () => { }); it('should enrich curated apps with rich display data', async () => { - // The curated index has a Data Enrichment entry with identifier - // a1b2c3d4-0000-0000-0000-000000000001 — create a matching registration const curatedUid = 'a1b2c3d4-0000-0000-0000-000000000001'; await insertCatalogRegistration({ universalIdentifier: curatedUid, name: 'Data Enrichment', sourcePackage: '@twentyhq/app-data-enrichment', + marketplaceDisplayData: { + icon: 'IconSparkles', + version: '1.0.0', + category: 'Data', + }, }); const res = await gqlRequest(MARKETPLACE_QUERY).expect(200);