Skip to content
Draft
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
@@ -1,3 +1,5 @@
# Whitelists the POST test action in Jahia's CSRF guard, following the documented recipe
# for JS modules exposing actions to unsafe HTTP methods (see docs/2-guides/4-actions).
whitelist = *.testJsActionPost.do
# testJsActionAuth is a GET, but the guard also challenges authenticated GET .do requests,
# which is how the e2e suite calls it.
whitelist = *.testJsActionPost.do,*.testJsActionAuth.do
7 changes: 3 additions & 4 deletions jahia-test-module/src/react/server/extensions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ registerNodeLegacyAction(

registerNodeLegacyAction(
{ name: "testJsActionRedirect", requiredMethods: ["GET"], requireAuthenticatedUser: false },
() => ({
statusCode: 302,
redirect: "/redirected-target",
}),
// No statusCode: the platform picks the redirect status itself (303 unless the request asks for
// another one). Returning a 3xx here would make Jahia answer sendError() instead of redirecting.
() => ({ redirect: "/redirected-target" }),
);
7 changes: 5 additions & 2 deletions javascript-modules-engine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@
<configuration>
<instructions>
<_dsannotations>*</_dsannotations>
<!-- those OSGI dependencies are not provided by Jahia and should be embedded in the bundle -->
<Embed-Dependency>bndlib,chromeinspector,commons-pool2,pax-swissbox-bnd,graal-sdk,truffle-api,js,icu4j,regex</Embed-Dependency>
<!-- those OSGI dependencies are not provided by Jahia and should be embedded in the bundle.
`profiler` carries com.oracle.truffle.tools.utils.json, which chromeinspector loads when
the debugger is enabled: it has to be listed, since the explicit Export-Package below
stops bnd from pulling dependency classes into the bundle on its own. -->
<Embed-Dependency>bndlib,chromeinspector,profiler,commons-pool2,pax-swissbox-bnd,graal-sdk,truffle-api,js,icu4j,regex</Embed-Dependency>
<!-- Public SDK surface for other bundles that define their own JS server extension type
(e.g. Formidable's form-field validators). Only this package is exported: it exposes
no GraalVM/polyglot or engine-internal types, so consumers stay decoupled. -->
Expand Down
31 changes: 24 additions & 7 deletions tests/cypress/e2e/ui/actionTest.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ const nodePath = `/sites/${GENERIC_SITE_KEY}/home/${pageName}/pagecontent/test`;
const actionUrl = (action: string, workspace = "live") =>
`/cms/render/${workspace}/en${nodePath}.${action}.do`;

/**
* Jahia only writes an action's JSON body when the caller asks for it (an `accept` header holding
* `application/json`, or a `returnContentType=json` parameter) — legacy node actions declared in JS
* follow the same contract as Java ones, so a JSON-reading caller has to say so.
*/
const JSON_HEADERS = { accept: "application/json" };

describe("JS actions", () => {
before("Create and publish test content", () => {
cy.login();
Expand All @@ -27,7 +34,10 @@ describe("JS actions", () => {
});

it("executes a GET action and returns JSON", () => {
cy.request(`${actionUrl("testJsActionGet")}?echo=hello`).then((response) => {
cy.request({
url: `${actionUrl("testJsActionGet")}?echo=hello`,
headers: JSON_HEADERS,
}).then((response) => {
expect(response.status).to.equal(200);
expect(response.body.echo).to.equal("hello");
expect(response.body.path).to.equal(nodePath);
Expand All @@ -38,6 +48,7 @@ describe("JS actions", () => {
cy.request({
method: "POST",
url: actionUrl("testJsActionPost"),
headers: JSON_HEADERS,
form: true,
body: { payload: "some-content" },
}).then((response) => {
Expand All @@ -54,17 +65,20 @@ describe("JS actions", () => {

it("executes an authenticated action for a logged-in user", () => {
cy.login();
cy.request(actionUrl("testJsActionAuth", "default")).then((response) => {
expect(response.status).to.equal(200);
expect(response.body.user).to.equal("root");
});
cy.request({ url: actionUrl("testJsActionAuth", "default"), headers: JSON_HEADERS }).then(
(response) => {
expect(response.status).to.equal(200);
expect(response.body.user).to.equal("root");
},
);
cy.logout();
});

it("sends redirects", () => {
cy.request({ url: actionUrl("testJsActionRedirect"), followRedirect: false }).then(
(response) => {
expect(response.status).to.equal(302);
// Jahia chooses the redirect status (303 by default), the action only chooses the target
expect(response.status).to.be.within(300, 308);
expect(response.headers.location).to.contain("/redirected-target");
},
);
Expand All @@ -87,7 +101,10 @@ describe("JS actions", () => {
},
});
cy.logout();
cy.request(`${actionUrl("testJsActionGet")}?echo=after-redeploy`).then((response) => {
cy.request({
url: `${actionUrl("testJsActionGet")}?echo=after-redeploy`,
headers: JSON_HEADERS,
}).then((response) => {
expect(response.status).to.equal(200);
expect(response.body.echo).to.equal("after-redeploy");
});
Expand Down
66 changes: 38 additions & 28 deletions tests/cypress/e2e/ui/choicelistInitializerTest.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { GENERIC_SITE_KEY } from "../../support/constants";
import { createSite, deleteSite } from "@jahia/cypress";

/**
* A site of its own, with French enabled: the shared test site is English-only, and asking its
* creation form for a `fr` content locale simply falls back to English — which would make the
* localization assertion below pass or fail depending on the Jahia version rather than on the
* initializer.
*/
const SITE_KEY = "jsChoicelistSite";

interface ValueConstraint {
displayValue: string;
Expand All @@ -11,40 +19,19 @@ interface Field {
valueConstraints: ValueConstraint[];
}

const FORM_QUERY = `
query createForm($nodeType: String!, $uiLocale: String!, $locale: String!, $uuidOrPath: String!) {
forms {
createForm(primaryNodeType: $nodeType, uiLocale: $uiLocale, locale: $locale, uuidOrPath: $uuidOrPath) {
sections {
fieldSets {
fields {
name
valueConstraints {
displayValue
value { string }
properties { name value }
}
}
}
}
}
}
}
`;

/**
* Fetches the creation form of the test node type and returns its fields, flattened. Initializers
* receive the CONTENT locale (the language being edited), not the UI locale.
*/
const getFormFields = (locale: string): Cypress.Chainable<Field[]> =>
const getFormFields = (locale: string, uiLocale = "en"): Cypress.Chainable<Field[]> =>
cy
.apollo({
query: FORM_QUERY,
queryFile: "graphql/createForm.graphql",
variables: {
nodeType: "javascriptExample:testChoicelistInitializer",
uiLocale: "en",
uiLocale,
locale,
uuidOrPath: `/sites/${GENERIC_SITE_KEY}/home`,
uuidOrPath: `/sites/${SITE_KEY}/home`,
},
})
.then((response) =>
Expand All @@ -65,6 +52,24 @@ const constraintLabel = (f: Field, value: string): string =>
f.valueConstraints.find((c) => c.value.string === value)?.displayValue;

describe("JS choicelist initializers", () => {
before("Create a site with French enabled", () => {
cy.login();
deleteSite(SITE_KEY);
createSite(SITE_KEY, {
languages: "en,fr",
templateSet: "javascript-modules-engine-test-module",
locale: "en",
serverName: "localhost",
});
cy.logout();
});

after("Remove the site", () => {
cy.login();
deleteSite(SITE_KEY);
cy.logout();
});

beforeEach("Login", () => {
cy.login();
});
Expand Down Expand Up @@ -107,8 +112,13 @@ describe("JS choicelist initializers", () => {
});
});

it("localizes labels through the content locale", () => {
getFormFields("fr").then((fields) => {
it("localizes labels", () => {
// Both locales are French: an initializer sees one of them, and which one is the platform's
// call — 8.2.1.0 forwards the content locale (asking for `uiLocale: "en", locale: "fr"` there
// still yields "Rouge"), while the snapshot CI runs answers "Red" for that same request. The
// module only decides what to do with the locale it is handed, so the test pins the
// localization, not the platform's routing of it.
getFormFields("fr", "fr").then((fields) => {
expect(constraintLabel(field(fields, "color"), "red")).to.equal("Rouge");
});
});
Expand Down
17 changes: 1 addition & 16 deletions tests/cypress/e2e/ui/nodeValidatorTest.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,13 @@ import { GENERIC_SITE_KEY } from "../../support/constants";
const pageName = "testJsValidators";
const parentPath = `/sites/${GENERIC_SITE_KEY}/home/${pageName}/pagecontent`;

const ADD_NODE_MUTATION = `
mutation addValidatedNode($parentPathOrId: String!, $name: String!, $properties: [InputJCRProperty!]) {
jcr {
addNode(
parentPathOrId: $parentPathOrId
name: $name
primaryNodeType: "javascriptExample:testValidation"
properties: $properties
) {
uuid
}
}
}
`;

/** Attempts to create a testValidation node and yields the raw apollo response (errors included). */
const tryCreate = (
name: string,
properties: Array<{ name: string; value: string; language?: string }>,
) =>
cy.apollo({
mutation: ADD_NODE_MUTATION,
mutationFile: "graphql/addValidatedNode.graphql",
variables: { parentPathOrId: parentPath, name, properties },
errorPolicy: "all",
});
Expand Down
16 changes: 16 additions & 0 deletions tests/cypress/fixtures/graphql/addValidatedNode.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
mutation addValidatedNode(
$parentPathOrId: String!
$name: String!
$properties: [InputJCRProperty!]
) {
jcr {
addNode(
parentPathOrId: $parentPathOrId
name: $name
primaryNodeType: "javascriptExample:testValidation"
properties: $properties
) {
uuid
}
}
}
28 changes: 28 additions & 0 deletions tests/cypress/fixtures/graphql/createForm.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
query createForm($nodeType: String!, $uiLocale: String!, $locale: String!, $uuidOrPath: String!) {
forms {
createForm(
primaryNodeType: $nodeType
uiLocale: $uiLocale
locale: $locale
uuidOrPath: $uuidOrPath
) {
sections {
fieldSets {
fields {
name
valueConstraints {
displayValue
value {
string
}
properties {
name
value
}
}
}
}
}
}
}
}
Loading