fix: update integration tests and exception filter for refactored types

- 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
This commit is contained in:
Félix Malfait
2026-03-04 13:29:28 +01:00
parent 682bcd0186
commit 97f3bc3da5
4 changed files with 78 additions and 12 deletions
@@ -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;
@@ -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<Response>();
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,
);
}
}
}
@@ -75,7 +75,7 @@ const insertRegistrationWithSource = async (
params: {
universalIdentifier: string;
name: string;
sourceType: 'npm' | 'tarball' | 'none';
sourceType: 'npm' | 'tarball' | 'local';
sourcePackage?: string;
},
): Promise<string> => {
@@ -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);
@@ -58,6 +58,7 @@ describe('Marketplace Catalog Sync (integration)', () => {
name: string;
sourcePackage: string;
latestAvailableVersion?: string;
marketplaceDisplayData?: Record<string, unknown>;
}): Promise<string> => {
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);