Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ function __completeTask(action) {
__dialogs.closeWindow();
window.close();
}).catch((error) => {
const message = error && error.data && error.data.message ? error.data.message : 'Unknown error';
const message = App.services.apiErrors.refusalMessageFor(error, 'Submit failed.');
__notifications.show({ type: 'negative', title: 'Submit failed', description: message });
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.intent.generator.form;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Map;

import org.eclipse.dirigible.components.intent.model.IntentModel;
import org.eclipse.dirigible.components.intent.parser.IntentParser;
import org.junit.jupiter.api.Test;

/**
* Verifies that a task form's COMPLETE catch routes a failed {@code POST /services/inbox/tasks/*}
* through the shared, safe {@code apiErrors.refusalMessageFor} instead of printing the raw
* developer-facing {@code error.data.message} a 500 carries (issue #7296, a #7263 follow-up).
*
* #7151/#7062 made the shared apiErrors helper safe and wired it into the power form/dialog; #7152
* reached the personal/partner line dialogs. The one path every process user task actually takes -
* {@code __completeTask}'s own {@code .catch} - was left printing
* {@code error && error.data && error.data.message}, a spelling the emission-coverage walk's string
* checks could not see either (neither "(e && e.message)" nor "String(e.message" matches it), which
* is why this generator-level unit test exists rather than relying on that walk alone.
*/
class TaskFormApiErrorTest {

private static final String YAML = """
name: sales
entities:
- name: SalesOrder
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: total, type: decimal }
processes:
- name: OrderApproval
trigger: { onCreate: SalesOrder }
steps:
- { name: confirm, kind: userTask, args: { assignee: manager, form: ConfirmOrder } }
- { name: end, kind: end }
forms:
- { name: ConfirmOrder, forEntity: SalesOrder, fields: [total], actions: [confirm] }
""";

private static String codeOf(String form) {
IntentModel model = IntentParser.parse(YAML);
Map<String, Map<String, Object>> forms = FormIntentGenerator.buildFormsForTest(model);
return String.valueOf(forms.get(form)
.get("code"));
}

@Test
void completeTaskCatchRoutesThroughApiErrors() {
String code = codeOf("ConfirmOrder");

assertTrue(code.contains("App.services.apiErrors.refusalMessageFor(error, 'Submit failed.')"),
"the COMPLETE catch must map the failure through the shared apiErrors helper: " + code);
assertFalse(code.contains("error.data.message"),
"the COMPLETE catch must never read the developer-facing error.data.message: " + code);
assertFalse(code.contains("'Unknown error'"), "the raw 'Unknown error' fallback must be gone: " + code);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"feeds": [],
"scripts": [],
"code": "${dollar}scope.model.param1 = \"\";\n${dollar}scope.model.param2 = 0;\n\n${dollar}scope.onTriggerClicked = function(){\n ${dollar}http.post(\"/services/java/${projectName}/${javaPackageName}/ProcessService/processes\", JSON.stringify(${dollar}scope.model)).then(function (response) {\n alert(\"A new process instance has been triggered.\\nResponse: \" + JSON.stringify(response.data));\n }, function (error) {\n alert(`Unable to trigger a new process: '${dollar}{(error && error.data && error.data.message) || error}'`);\n });\n}\n",
"code": "${dollar}scope.model.param1 = \"\";\n${dollar}scope.model.param2 = 0;\n\n${dollar}scope.onTriggerClicked = function(){\n ${dollar}http.post(\"/services/java/${projectName}/${javaPackageName}/ProcessService/processes\", JSON.stringify(${dollar}scope.model)).then(function (response) {\n alert(\"A new process instance has been triggered.\\nResponse: \" + JSON.stringify(response.data));\n }, function (error) {\n alert('Unable to trigger a new process: ' + App.services.apiErrors.refusalMessageFor(error, 'Unable to trigger a new process.'));\n });\n}\n",
"form": [
{
"controlId": "header",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,21 @@ function formController(ctx) {
// AngularJS resolves $http with { data, status, statusText, ... } and legacy .form code branches
// on the status (`if (response.status != 202)`), so the success path must carry it too - not just
// the rejection path. ctx.http resolves the body alone, hence the exchange() call here.
//
// The rejection carries the ApiError shape App.services.apiErrors reads (httpStatus/errorMessage)
// on the thrown object ITSELF, so a .form's own .catch can hand it straight to
// refusalMessageFor(error, '<its own fallback>') - never read .data.message, the raw
// developer-facing sentence a 500 carries (#7296). The legacy .data.message field stays too, for
// .form code authored against the old AngularJS $http shim before this fix existed.
const wrap = (p) => p.then((r) => ({ data: r.data, status: r.status }))
.catch((e) => { throw { data: { message: e && e.message }, status: e && e.httpStatus }; });
.catch((e) => {
throw {
data: { message: e && e.message },
status: e && e.httpStatus,
httpStatus: e && e.httpStatus,
errorMessage: e && e.errorMessage,
};
});
const $http = {
get: (url) => wrap(harmoniaHttp.exchange('GET', url, undefined)),
post: (url, data) => wrap(harmoniaHttp.exchange('POST', url, data)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3020,10 +3020,19 @@ private void assertEmission() {
emittedPages.contains("gen/emission/admin/index.html")
&& emittedPages.contains("gen/emission/js/components/pages/my/ClaimMyListPage.js"),
"the generated-page walk must cover the admin page and the SPA pages, else the rule below is vacuous: " + emittedPages);
// #7296: `error.data.message` (a BPM task form's old .catch, and the old template-bpm
// scaffold's alert()) is a THIRD spelling of the same defect that neither pattern below
// matched. It does not collide with the $http shim's own `message: e && e.message` field
// WRITE (form.js.template), which every generated form.js legitimately still carries for
// .form code authored against the old AngularJS $http compat shape - that is a different
// token sequence. FormIntentGenerator's own fix is unit-tested directly (no `forms:` /
// userTask form binding exists in this fixture to exercise gen/.../forms/*/form.js) - see
// TaskFormApiErrorTest.
List<String> rawMessagePages = emittedPages.stream()
.filter(page -> {
String content = contentOf(page);
return content.contains("(e && e.message)") || content.contains("String(e.message");
return content.contains("(e && e.message)") || content.contains("String(e.message")
|| content.contains("error.data.message");
})
.toList();
assertTrue(rawMessagePages.isEmpty(),
Expand Down
Loading