diff --git a/.github/actions/core-cicd/maven-job/action.yml b/.github/actions/core-cicd/maven-job/action.yml index eade3711b4d9..5b7ea748cd18 100644 --- a/.github/actions/core-cicd/maven-job/action.yml +++ b/.github/actions/core-cicd/maven-job/action.yml @@ -321,4 +321,4 @@ runs: name: "build-reports-test-${{ inputs.stage-name }}" path: | **/target/jacoco-report/*.exec - **/target/*-reports/TEST-*.xml \ No newline at end of file + **/target/*-reports/*.xml \ No newline at end of file diff --git a/.github/filters.yaml b/.github/filters.yaml index 7c8af5d27894..12bc07f66f43 100644 --- a/.github/filters.yaml +++ b/.github/filters.yaml @@ -12,6 +12,7 @@ backend: &backend - 'core-web/pom.xml' - 'dotCMS/src/main/webapp/html/**/!(*.{css,js})' - 'dotcms-postman/**' + - 'test-karate/**' - 'e2e/**' - 'dotCMS/!(src/main/webapp/html/)**' - 'dotcms-integration/**' diff --git a/.github/workflows/cicd_1-pr.yml b/.github/workflows/cicd_1-pr.yml index ad2fb8dbcedc..a0fe541d77ea 100644 --- a/.github/workflows/cicd_1-pr.yml +++ b/.github/workflows/cicd_1-pr.yml @@ -67,6 +67,7 @@ jobs: jvm_unit_test: ${{ needs.initialize.outputs.jvm_unit_test == 'true' }} integration: ${{ needs.initialize.outputs.backend == 'true' }} postman: ${{ needs.initialize.outputs.backend == 'true' }} + karate: ${{ needs.initialize.outputs.backend == 'true' }} frontend: ${{ needs.initialize.outputs.frontend == 'true' }} cli: ${{ needs.initialize.outputs.cli == 'true' }} e2e: ${{ needs.initialize.outputs.build == 'true' }} diff --git a/.github/workflows/cicd_2-merge-queue.yml b/.github/workflows/cicd_2-merge-queue.yml index 4251488f1405..1542418ccfe8 100644 --- a/.github/workflows/cicd_2-merge-queue.yml +++ b/.github/workflows/cicd_2-merge-queue.yml @@ -24,6 +24,7 @@ jobs: jvm_unit_test: ${{ needs.initialize.outputs.jvm_unit_test == 'true' }} integration: ${{ needs.initialize.outputs.backend == 'true' }} postman: ${{ needs.initialize.outputs.backend == 'true' }} + karate: ${{ needs.initialize.outputs.backend == 'true' }} frontend: ${{ needs.initialize.outputs.frontend == 'true' }} cli: ${{ needs.initialize.outputs.cli == 'true' }} e2e: ${{ needs.initialize.outputs.build == 'true' }} diff --git a/.github/workflows/cicd_comp_test-phase.yml b/.github/workflows/cicd_comp_test-phase.yml index f54fe34a247e..a588be0db236 100644 --- a/.github/workflows/cicd_comp_test-phase.yml +++ b/.github/workflows/cicd_comp_test-phase.yml @@ -37,6 +37,10 @@ on: required: false type: boolean default: false + karate: + required: false + type: boolean + default: false integration: required: false type: boolean @@ -180,7 +184,38 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} artifacts-from: ${{ env.ARTIFACT_RUN_ID }} cleanup-runner: true - + # Karate Tests + karate-tests: + name: Karate Tests - ${{ matrix.suites.name }} + runs-on: ubuntu-24.04 + if: inputs.karate || inputs.run-all-tests + strategy: + fail-fast: false + matrix: + suites: + - { name: "Default", pathName: "default", tests: 'KarateCITests#defaults' } + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/core-cicd/maven-job + with: + stage-name: "Karate ${{ matrix.suites.name }}" + maven-args: "verify -pl :dotcms-test-karate -Dkarate.test.skip=false -Dit.test=${{ matrix.suites.tests }}" + generates-test-results: true + dotcms-license: ${{ secrets.DOTCMS_LICENSE }} + requires-node: true + needs-docker-image: true + github-token: ${{ secrets.GITHUB_TOKEN }} + artifacts-from: ${{ env.ARTIFACT_RUN_ID }} + cleanup-runner: true + - id: upload-karate-report + name: Upload Karate-report + uses: actions/upload-artifact@v4 + with: + name: karate-reports + path: dotCMS/target/karate-reports # E2E Tests linux-e2e-tests: name: E2E Tests ${{matrix.suites.name}} diff --git a/e2e/dotcms-e2e-java/pom.xml b/e2e/dotcms-e2e-java/pom.xml index 140c023ba7a1..bf8e9c108eca 100644 --- a/e2e/dotcms-e2e-java/pom.xml +++ b/e2e/dotcms-e2e-java/pom.xml @@ -102,26 +102,6 @@ junit-vintage-engine test - - org.junit.jupiter - junit-jupiter-params - test - - - org.junit.platform - junit-platform-suite - test - - - org.junit.vintage - junit-vintage-engine - test - - - org.junit.platform - junit-platform-suite - test - com.microsoft.playwright playwright diff --git a/justfile b/justfile index 0730a5308de5..881932955386 100644 --- a/justfile +++ b/justfile @@ -48,7 +48,7 @@ build-prod: # Runs a comprehensive test suite including core integration and postman tests, suitable for final validation build-test-full: - ./mvnw clean install -Dcoreit.test.skip=false -Dpostman.test.skip=false + ./mvnw clean install -Dcoreit.test.skip=false -Dpostman.test.skip=false -Dkarate.test.skip=false # Builds a specified module without its dependencies, defaulting to the core server (dotcms-core) build-select-module module="dotcms-core": @@ -99,6 +99,9 @@ dev-tomcat-stop: test-postman collections='page': ./mvnw -pl :dotcms-postman verify -Dpostman.test.skip=false -Pdebug -Dpostman.collections={{ collections }} +test-karate collections='KarateCITests#defaults': + ./mvnw -pl :dotcms-test-karate verify -Dkarate.test.skip=false -Pdebug -Dit.test={{ collections }} + # Stops Postman-related Docker containers postman-stop: ./mvnw -pl :dotcms-postman -Pdocker-stop -Dpostman.test.skip=false @@ -121,7 +124,13 @@ build-core-only: # Prepares the environment for running integration tests in an IDE test-integration-ide: - ./mvnw -pl :dotcms-integration pre-integration-test -Dcoreit.test.skip=false + ./mvnw -pl :dotcms-integration pre-integration-test -Dcoreit.test.skip=false -Dtomcat.port=8080 + +test-postman-ide: + ./mvnw -pl :dotcms-test-karate pre-integration-test -Dpostman.test.skip=false -Dtomcat.port=8080 + +test-karate-ide: + ./mvnw -pl :dotcms-test-karate pre-integration-test -Dkarate.test.skip=false -Dtomcat.port=8080 # Stops integration test services test-integration-stop: diff --git a/pom.xml b/pom.xml index 77fd5695e41e..8d9912d177f2 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ reports e2e/dotcms-e2e-java e2e/dotcms-e2e-node + test-karate diff --git a/test-karate/pom.xml b/test-karate/pom.xml new file mode 100644 index 000000000000..0a7c64b3aed8 --- /dev/null +++ b/test-karate/pom.xml @@ -0,0 +1,203 @@ + + + 4.0.0 + + com.dotcms + dotcms-parent + ${revision}${sha1}${changelist} + ../parent/pom.xml + + + jar + dotcms-test-karate + + + 21 + 21 + 21 + + true + true + true + true + + 8080 + false + 1.5.0 + true + http://localhost:${tomcat.port} + KarateCITests#defaults + + + + + + src/test/java + + **/*.java + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + io.fabric8 + docker-maven-plugin + + ${karate.test.skip} + true + true + + + + + -XX:+PrintFlagsFinal + 15 + FORCE + false + false + 600 + obfuscate_me + true + http://localhost:${tomcat.port} + true + true + http://wm:8080/c + http://wm:${tomcat.port}/i + http://wm:${tomcat.port}/e + http://wm:${tomcat.port}/m + + + + + + + + cleanup-at-start + + stop + volume-remove + + pre-integration-test + + + start + + start + + pre-integration-test + + + stop + + stop + + + verify + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0 + + ${karate.test.skip} + + ${karate.base.url} + + + **/*.java + + false + + + + integration-tests + integration-test + + integration-test + + + + verify-tests + verify + + verify + + + + + + + + + + + org.junit.jupiter + junit-jupiter-engine + test + 5.10.2 + + + io.karatelabs + karate-junit5 + ${karate.version} + test + + + + + + + \ No newline at end of file diff --git a/test-karate/src/test/java/KarateCITests.java b/test-karate/src/test/java/KarateCITests.java new file mode 100644 index 000000000000..a2a990e0fe84 --- /dev/null +++ b/test-karate/src/test/java/KarateCITests.java @@ -0,0 +1,20 @@ +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import org.junit.jupiter.api.Test; + +public class KarateCITests { + // Note this is a JUnit 5 test compare with @Karate.Test annotated methods.https://stackoverflow.com/questions/65577487/whats-the-purpose-of-karate-junit5-when-you-can-run-tests-without-it + + @Test + void defaults() { + Results results = Runner.path("classpath:tests/defaults").tags("~@ignore") + .outputHtmlReport(true) + .outputJunitXml(true) + .outputCucumberJson(true) + .parallel(1); + assertEquals(0, results.getFailCount(), results.getErrorMessages()); + } + +} diff --git a/test-karate/src/test/java/dependencyfeatures/newContent.feature b/test-karate/src/test/java/dependencyfeatures/newContent.feature new file mode 100644 index 000000000000..de03b11bddfc --- /dev/null +++ b/test-karate/src/test/java/dependencyfeatures/newContent.feature @@ -0,0 +1,30 @@ +Feature: Create content dependencies for tests + + Background: + * def authString = 'admin@dotcms.com:admin' + * def encodedAuth = function(s) { return java.util.Base64.getEncoder().encodeToString(s.getBytes('UTF-8')); } + * def baseUrl = 'http://localhost:8080' + + Scenario: SuccessRequest + + # Adding the content dependencies + Given url baseUrl + '/api/v1/workflow/actions/default/fire/PUBLISH' + + #Variables to set timestamp name in the body request + * def DateTimeFormatter = Java.type('java.time.format.DateTimeFormatter') + * def LocalDateTime = Java.type('java.time.LocalDateTime') + * def formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") + * def now = LocalDateTime.now() + * def timestamp = now.format(formatter) + * def timestampString = 'SucessRequest' + timestamp + + And request { "contentlet": { "stInode" : "f4d7c1b8-2c88-4071-abf1-a5328977b07d", "languageId" : 1, "key": "#(timestampString)", "value": "#(timestampString)" } } + And header Content-Type = 'application/json' + And header Authorization = 'Basic ' + encodedAuth(authString) + When method put + Then status 200 + #* print 'Response:', response + And match response.errors == [] + And match response.entity.title contains("#(timestampString)") + * def contentId = response.entity.identifier + * def contentInode = response.entity.inode \ No newline at end of file diff --git a/test-karate/src/test/java/dependencyfeatures/turnOffSystemTableProperties.feature b/test-karate/src/test/java/dependencyfeatures/turnOffSystemTableProperties.feature new file mode 100644 index 000000000000..41cb944c425e --- /dev/null +++ b/test-karate/src/test/java/dependencyfeatures/turnOffSystemTableProperties.feature @@ -0,0 +1,16 @@ +Feature: Turn off system table properties for default content to default language + + Background: + * def authString = 'admin@dotcms.com:admin' + * def encodedAuth = function(s) { return java.util.Base64.getEncoder().encodeToString(s.getBytes('UTF-8')); } + * def baseUrl = 'http://localhost:8080' + + Scenario: TurnOFF_DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE + + Given url baseUrl + '/api/v1/system-table' + And request { key: 'DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE', value: false } + And header Content-Type = 'application/json' + And header Authorization = 'Basic ' + encodedAuth(authString) + When method POST + Then status 200 + And match response.entity contains("DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE saved/updated") \ No newline at end of file diff --git a/test-karate/src/test/java/dependencyfeatures/turnOnSystemTableProperties.feature b/test-karate/src/test/java/dependencyfeatures/turnOnSystemTableProperties.feature new file mode 100644 index 000000000000..38d444bb0c36 --- /dev/null +++ b/test-karate/src/test/java/dependencyfeatures/turnOnSystemTableProperties.feature @@ -0,0 +1,16 @@ +Feature: turnOnSystemTableProperties + + Background: + * def authString = 'admin@dotcms.com:admin' + * def encodedAuth = function(s) { return java.util.Base64.getEncoder().encodeToString(s.getBytes('UTF-8')); } + * def baseUrl = 'http://localhost:8080' + + Scenario: TurnON_DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE + + Given url baseUrl + '/api/v1/system-table' + And request { key: 'DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE', value: true } + And header Content-Type = 'application/json' + And header Authorization = 'Basic ' + encodedAuth(authString) + When method POST + Then status 200 + And match response.entity contains("DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE saved/updated") \ No newline at end of file diff --git a/test-karate/src/test/java/karate-config.js b/test-karate/src/test/java/karate-config.js new file mode 100644 index 000000000000..d28f0028725c --- /dev/null +++ b/test-karate/src/test/java/karate-config.js @@ -0,0 +1,19 @@ +function fn() { + var env = karate.env; // get system property 'karate.env' + karate.log('karate.env system property was:', env); + if (!env) { + env = 'dev'; + } + var config = { + env: env, + baseUrl: karate.properties['karate.base.url'] || 'http://localhost:8080' + } + if (env == 'dev') { + // customize + // e.g. config.foo = 'bar'; + } else if (env == 'e2e') { + // customize + } + karate.log('Base URL set to:', config.baseUrl); + return config; +} \ No newline at end of file diff --git a/test-karate/src/test/java/logback-test.xml b/test-karate/src/test/java/logback-test.xml new file mode 100644 index 000000000000..7a07cdecd487 --- /dev/null +++ b/test-karate/src/test/java/logback-test.xml @@ -0,0 +1,26 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + target/tests.log + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-karate/src/test/java/tests/defaults/CheckingJSONAttributes.feature b/test-karate/src/test/java/tests/defaults/CheckingJSONAttributes.feature new file mode 100644 index 000000000000..1937522db2aa --- /dev/null +++ b/test-karate/src/test/java/tests/defaults/CheckingJSONAttributes.feature @@ -0,0 +1,112 @@ +Feature: Checking JSON Attributes + + Background: + * def authString = 'admin@dotcms.com:admin' + * def encodedAuth = function(s) { return java.util.Base64.getEncoder().encodeToString(s.getBytes('UTF-8')); } + * def baseUrl = 'http://localhost:8080' + * def commonHeaders = { 'Content-Type': 'application/json'} + * commonHeaders['Authorization'] = 'Basic ' + encodedAuth(authString) + * def extractErrors = + """ + function(response) { + var errors = []; + var results = response.entity.results; + if (results && results.length > 0) { + for (var i = 0; i < results.length; i++) { + var result = results[i]; + // Handle both nested error messages and direct error messages + for (var key in result) { + if (result[key] && result[key].errorMessage) { + errors.push(result[key].errorMessage); + } + } + } + } + return errors; + } + """ + + Scenario: Checking Content Audit Attributes + Given url baseUrl + '/api/v1/workflow/actions/default/fire/PUBLISH' + And request + """ + { + "contentlet": [ + { + "contentType": "webPageContent", + "title": "Test Generic Content", + "contentHost": "default", + "body": "This is my Test Generic Content" + } + ] + } + """ + And headers commonHeaders + When method post + Then status 200 + * def firstResult = response.entity.results[0] + * def contentId = karate.keysOf(firstResult)[0] + * def contentIdentifier = firstResult[contentId].identifier + Given url baseUrl + '/api/v1/content/' + contentIdentifier + And headers commonHeaders + When method get + Then status 200 + And match response.entity contains + """ + { + "contentType": "webPageContent", + "title": "Test Generic Content", + "hostName": "default", + "body": "This is my Test Generic Content", + "creationDate": "#notnull", + "owner": "#notnull", + "ownerName": "#notnull", + "modDate": "#notnull", + "modUser": "#notnull", + "modUserName": "#notnull", + "publishDate": "#notnull", + "publishUser": "#notnull", + "publishUserName": "#notnull" + } + """ + + Scenario Outline: Testing Content Creation Validation + Given url baseUrl + '/api/v1/workflow/actions/default/fire/PUBLISH' + And request + And headers commonHeaders + When method post + Then status 200 + * def errors = call extractErrors response + * match errors contains + + Examples: + | payload | expectedError | + | { "contentlet": [{}] } | 'Content Type does not exist' | + | { "contentlet": [{ "contentType": "webPageContent", "contentHost": "default" }] } | 'Contentlet with ID \'Unknown/New\' [\'\'] has invalid/missing field(s). - Fields: [REQUIRED]: Title (title), Body (body)' | + | { "contentlet": [{ "contentType": "webPageContent", "title": "", "contentHost": "default" }] } | 'Contentlet with ID \'Unknown/New\' [\'\'] has invalid/missing field(s). - Fields: [REQUIRED]: Title (title), Body (body)' | + + Scenario: Retrieving Test Generic Content - Wrong ID + Given url baseUrl + '/api/v1/content/wrongID' + And headers commonHeaders + When method get + Then status 404 + And match response.message contains 'The contentlet wrongID and language 1 does not exist' + + Scenario: Creating Test Generic Content - No auth + Given url baseUrl + '/api/v1/workflow/actions/default/fire/PUBLISH' + And request + """ + { + "contentlet": [ + { + "contentType": "webPageContent", + "title": "Test Generic Content", + "contentHost": "default" + } + ] + } + """ + And header Content-Type = 'application/json' + When method post + Then status 401 + And match response contains 'CONTENT_APIS_ALLOW_ANONYMOUS permission exceeded' \ No newline at end of file diff --git a/test-karate/src/test/java/tests/defaults/CheckingJSONAttributesRunner.java b/test-karate/src/test/java/tests/defaults/CheckingJSONAttributesRunner.java new file mode 100644 index 000000000000..1ea28fdd13a4 --- /dev/null +++ b/test-karate/src/test/java/tests/defaults/CheckingJSONAttributesRunner.java @@ -0,0 +1,12 @@ +package tests.defaults; + +import com.intuit.karate.junit5.Karate; + +public class CheckingJSONAttributesRunner { + + @Karate.Test + Karate testCheckingJSONAttributesRunner() { + return Karate.run("CheckingJSONAttributes").relativeTo(getClass()); + } + +} \ No newline at end of file diff --git a/test-karate/src/test/java/tests/defaults/CreateNewContents.feature b/test-karate/src/test/java/tests/defaults/CreateNewContents.feature new file mode 100644 index 000000000000..2cc807513e55 --- /dev/null +++ b/test-karate/src/test/java/tests/defaults/CreateNewContents.feature @@ -0,0 +1,60 @@ +Feature: Content Management API Tests + + Background: + * def authString = 'admin@dotcms.com:admin' + * def encodedAuth = function(s) { return java.util.Base64.getEncoder().encodeToString(s.getBytes('UTF-8')); } + * def baseUrl = baseUrl + '' + * def authHeader = 'Basic ' + encodedAuth(authString) + * def commonHeaders = { 'Content-Type': 'application/json', 'Authorization': '#(authHeader)' } + * def newContent = karate.callSingle('classpath:dependencyfeatures/newContent.feature') + * def contentId = newContent.contentId + * def contentInode = newContent.contentInode + + @smoke @positive + Scenario Outline: Verify content retrieval by with default language + # Turn on system properties + * call read('classpath:dependencyfeatures/turnOnSystemTableProperties.feature') + * configure cookies = null + Given url baseUrl + '/api/v1/content/' + + '?language=2' + And headers commonHeaders + When method get + Then status 200 + And match response.entity. == + + # Turn off system properties + * call read('classpath:dependencyfeatures/turnOffSystemTableProperties.feature') + + Examples: + | type | id | matchField | expectedValue | + | identifier | contentId | identifier | contentId | + | inode | contentInode | inode | contentInode | + + @negative @security + Scenario Outline: Verify authentication requirements for content update - + # Turn off system properties + * call read('classpath:dependencyfeatures/turnOffSystemTableProperties.feature') + * configure cookies = null + Given url baseUrl + '/api/v1/workflow/actions/default/fire/PUBLISH' + And request + """ + { + "contentlet": { + "stInode": "f4d7c1b8-2c88-4071-abf1-a5328977b07d", + "languageId": 1, + "identifier": '#(contentId)', + "key": "UpdatedKey", + "value": "Updatedvalue" + } + } + """ + * def auth = (useAuth == 'true') ? ('Basic ' + encodedAuth(credentials)) : null + And header Authorization = auth + And header Content-Type = 'application/json' + When method put + Then status 401 + And match response contains expectedError + + Examples: + | scenario | useAuth | credentials | expectedError | + | No Credentials | false | null | CONTENT_APIS_ALLOW_ANONYMOUS permission exceeded - system set to READ but WRITE was required | + | Wrong Credentials | true | 'admin@dotcms.com:admin1' | Authentication credentials are required | \ No newline at end of file diff --git a/test-karate/src/test/java/tests/defaults/CreateNewContentsRunner.java b/test-karate/src/test/java/tests/defaults/CreateNewContentsRunner.java new file mode 100644 index 000000000000..22102b060bf7 --- /dev/null +++ b/test-karate/src/test/java/tests/defaults/CreateNewContentsRunner.java @@ -0,0 +1,12 @@ +package tests.defaults; + +import com.intuit.karate.junit5.Karate; + +public class CreateNewContentsRunner { + + @Karate.Test + Karate testCreateNewContentsRunner() { + return Karate.run("CreateNewContents").relativeTo(getClass()); + } + +} \ No newline at end of file