From 311373589d7c198350eb73e6ca48de3a9c299803 Mon Sep 17 00:00:00 2001 From: Alex Kanitz Date: Fri, 13 Nov 2026 22:06:07 +0100 Subject: [PATCH 1/6] fix: check input sources without JavaScript The @ScriptAssert on TesInput needs a JavaScript engine, which Java 15 and later no longer have: on Java 21, every task with inputs failed with 500. An @AssertTrue method checks the same (a url that isn't blank, or content that isn't empty), so the message now names the field inputs[N].urlOrContentGiven. Tests for blank URLs and empty content added. Co-Authored-By: Claude Opus 5.5 --- .../ac/ebi/tsc/tesk/tes/model/TesInput.java | 15 ++++++++++++-- .../tesk/tes/api/TasksApiControllerTest.java | 10 +++++++++- .../invalid_tasks/inputs/blank_url.json | 20 +++++++++++++++++++ .../invalid_tasks/inputs/empty_content.json | 20 +++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 src/test/resources/invalid_tasks/inputs/blank_url.json create mode 100644 src/test/resources/invalid_tasks/inputs/empty_content.json diff --git a/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java b/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java index c0102052..e1f65e89 100644 --- a/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java +++ b/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java @@ -1,11 +1,11 @@ package uk.ac.ebi.tsc.tesk.tes.model; import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; -import org.hibernate.validator.constraints.ScriptAssert; import javax.validation.Valid; import javax.validation.constraints.*; @@ -18,7 +18,6 @@ */ @ApiModel(description = "Input describes Task input files.") @javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2021-03-24T17:10:08.716Z[Europe/London]") -@ScriptAssert(lang = "javascript", script = "(_this.url != null && (_this.url.trim().length) > 0) || (_this.content != null && _this.content.length > 0)", message = "URL or content required") public class TesInput { @JsonProperty("name") private String name; @@ -191,6 +190,18 @@ public void setContent(String content) { this.content = content; } + /** + * An input needs a source: a `url` that isn't blank, or a `content` that isn't empty. A plain + * method rather than a script (`@ScriptAssert`), as Java 15 and later have no JavaScript engine. + * + * @return whether `url` or `content` is given + */ + @JsonIgnore + @AssertTrue(message = "URL or content required") + public boolean isUrlOrContentGiven() { + return (url != null && !url.trim().isEmpty()) || (content != null && !content.isEmpty()); + } + @Override public boolean equals(Object o) { diff --git a/src/test/java/uk/ac/ebi/tsc/tesk/tes/api/TasksApiControllerTest.java b/src/test/java/uk/ac/ebi/tsc/tesk/tes/api/TasksApiControllerTest.java index 99d9b0b3..c26e47ec 100644 --- a/src/test/java/uk/ac/ebi/tsc/tesk/tes/api/TasksApiControllerTest.java +++ b/src/test/java/uk/ac/ebi/tsc/tesk/tes/api/TasksApiControllerTest.java @@ -243,7 +243,15 @@ public void createTask_invalid_inputs() throws Exception { String[][] files = new String[][] { new String[] {"no_path.json", "inputs[0].path"}, - new String[] {"no_source.json", "URL or content required"}, + new String[] { + "no_source.json", "inputs[0].urlOrContentGiven' URL or content required" + }, + new String[] { + "blank_url.json", "inputs[0].urlOrContentGiven' URL or content required" + }, + new String[] { + "empty_content.json", "inputs[0].urlOrContentGiven' URL or content required" + }, new String[] {"wrong_type.json", "inputs[0].type"}, new String[] {"empty_type.json", "inputs[0].type"} }; diff --git a/src/test/resources/invalid_tasks/inputs/blank_url.json b/src/test/resources/invalid_tasks/inputs/blank_url.json new file mode 100644 index 00000000..c9f3a253 --- /dev/null +++ b/src/test/resources/invalid_tasks/inputs/blank_url.json @@ -0,0 +1,20 @@ +{ + "executors": [ + { + "image": "ubuntu", + "command": [ + "echo", + "hello world" + ] + } + ], + "inputs": [ + { + "name": "input1", + "description": "Example", + "path": "/sth/sth.json", + "type": "FILE", + "url": " " + } + ] +} diff --git a/src/test/resources/invalid_tasks/inputs/empty_content.json b/src/test/resources/invalid_tasks/inputs/empty_content.json new file mode 100644 index 00000000..2161e399 --- /dev/null +++ b/src/test/resources/invalid_tasks/inputs/empty_content.json @@ -0,0 +1,20 @@ +{ + "executors": [ + { + "image": "ubuntu", + "command": [ + "echo", + "hello world" + ] + } + ], + "inputs": [ + { + "name": "input1", + "description": "Example", + "path": "/sth/sth.json", + "type": "FILE", + "content": "" + } + ] +} From 735bc6719146c6734c8cd99011a852f1a14e8fd4 Mon Sep 17 00:00:00 2001 From: Alex Kanitz Date: Fri, 13 Nov 2026 22:07:58 +0100 Subject: [PATCH 2/6] build: move to Java 21 Build, test and run on Java 21 (Eclipse Temurin 21 in the image, CI job Test on Java 21), with Spring Boot 2.7 as before: the first step of the move to Spring Boot 3 and later (#66). The new base image has the fixed apt, so the upgrade step goes. The unit tests open java.util and java.lang for system-rules' EnvironmentVariables. Lint stays on Java 8, as google-java-format 1.7 doesn't run on Java 21 and newer versions reformat the code. Docs and upgrade notes updated. Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 9 +++++---- .github/workflows/ci.yml | 9 +++++---- Dockerfile | 12 +++--------- README.md | 5 +++-- docs/deployment.md | 4 ++-- docs/development.md | 16 +++++++++------- docs/upgrading.md | 13 ++++++++++++- pom.xml | 9 +++++++-- 8 files changed, 46 insertions(+), 31 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e4d8e6dc..e370cc49 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,8 +3,8 @@ version: 2 # whatever the default branch is (`main` only moves forward to releases); # security updates always target the default branch, though updates: - # Dependencies in `pom.xml`. Java 8 and Spring Boot 2 stay until the move to - # Spring Boot 3, which needs code changes + # Dependencies in `pom.xml`. Spring Boot 2 stays until the move to Spring + # Boot 3, which needs code changes - package-ecosystem: maven directory: / schedule: @@ -38,8 +38,9 @@ updates: groups: github-actions: patterns: ["*"] - # Base images in the `Dockerfile` (tag and digest). Java 8 stays until the - # move to Spring Boot 3 + # Base images in the `Dockerfile` (tag and digest). Java 21 (LTS), as in + # `pom.xml` and the CI job `Test`: a new major version of Java is a + # decision of its own - package-ecosystem: docker directory: / schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9b28662..a098c8d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,9 +91,10 @@ jobs: docker run --rm --volume "${PWD}:/repo" --workdir /repo \ rhysd/actionlint:1.7.7@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 \ -color - # Java 8, as in `test`: the plugin versions are the last that run on it. - # No Maven cache: it would share the key of `test` and could be saved - # with only the lint plugins in it + # Java 8, unlike `test`: google-java-format 1.7 doesn't run on Java 21, + # and newer versions format the code differently (to be updated with the + # move to Spring Boot 3, #66). No Maven cache: it would share the key of + # `test` and could be saved with only the lint plugins in it - name: Set up Java uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0 with: @@ -129,7 +130,7 @@ jobs: uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0 with: distribution: temurin - java-version: "8" + java-version: "21" cache: maven - name: Build and run tests run: ./mvnw -B clean verify diff --git a/Dockerfile b/Dockerfile index 673860dc..5dc4966f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Builds the application from source, so that a plain `docker build .` is all -# that is needed. Java 8, as in `pom.xml`; the base images are pinned by digest +# that is needed. Java 21, as in `pom.xml`; the base images are pinned by digest # and updated by Dependabot -FROM eclipse-temurin:8-jdk@sha256:e9255b50040337b0dec91027868dca1952ef5441bf0c8f878c5f790eaedbfea2 AS build +FROM eclipse-temurin:21-jdk@sha256:0a3f329821dcadf54b2a3033a15e2e9f007069a9db43d7f51897e6b80e726b07 AS build # The Maven wrapper downloads the .zip of Maven and checks its SHA-256 sum # (`.mvn/wrapper/maven-wrapper.properties`); without `unzip` it would fetch the # .tar.gz instead, which the sum does not match @@ -15,13 +15,7 @@ COPY src src RUN ./mvnw -B -Dmaven.test.skip=true package \ && cp target/tesk-*.jar /app.jar -FROM eclipse-temurin:8-jre@sha256:377fe8e36a4b1530d9d52020069367ef44313fa891a82ae102b1016a6a3a72f5 -# The pinned base image has a vulnerable apt (CVE-2026-109518; fixed by -# 3.2.1ubuntu0.1 in the Ubuntu security updates); can go once Dependabot moves -# the base image to a build with the fix -RUN apt-get update && \ - apt-get install -y --no-install-recommends --only-upgrade apt libapt-pkg7.0 && \ - rm -rf /var/lib/apt/lists/* +FROM eclipse-temurin:21-jre@sha256:18bb4a91a3dea3f03ee31d3755af34dfb40f904fc77ab56cdc2f092e32fff9c2 # Run as non-root user; group 0 owns the home directory, so that arbitrary # user IDs (e.g., on OpenShift, always in group 0) can use it too. The app # writes only to `/tmp`; the Kubernetes client reads `~/.kube/config` (or diff --git a/README.md b/README.md index 82a0fc41..bc0bb56a 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,9 @@ To deploy TESK to a cluster, use the Helm chart of [TESK][tesk]. ## Development -TESK API builds and runs on Java 8. To build it and run the unit and -integration tests (no cluster needed): +TESK API builds and runs on Java 21 (the formatter and linter still need +Java 8; see [Development][docs-development]). To build it and run the unit +and integration tests (no cluster needed): ```bash ./mvnw clean verify diff --git a/docs/deployment.md b/docs/deployment.md index b9b760c3..0bc63f62 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -22,7 +22,7 @@ cosign verify elixircloud/tesk-api:1.2.0 \ For `dev` images, the identity is `https://github.com/elixir-cloud-aai/tesk-api/.github/workflows/ci.yml@refs/heads/dev`. -The image runs the application with Java 8 on [Eclipse +The image runs the application with Java 21 on [Eclipse Temurin](https://adoptium.net/) and listens on port 8080. `JAVA_OPTS` passes options to the JVM. It runs as the non-root user `tesk` (UID 1000, group 0, home directory `/home/tesk`); any other UID in group 0 works too, as on @@ -66,7 +66,7 @@ needs to manage jobs, pods and persistent volume claims. ## Running locally To run TESK API outside of Kubernetes, e.g. against a local cluster such as -[kind](https://kind.sigs.k8s.io/) or [k3d](https://k3d.io/), with Java 8 and +[kind](https://kind.sigs.k8s.io/) or [k3d](https://k3d.io/), with Java 21 and a `KUBECONFIG` file for that cluster: ```bash diff --git a/docs/development.md b/docs/development.md index 7c9b3f10..722da832 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,9 +1,8 @@ # Development TESK API is a [Spring Boot](https://spring.io/projects/spring-boot) 2.7 -application on Java 8, built with Maven 3.9 (through the wrapper `./mvnw`, -which downloads the version pinned in `.mvn/wrapper/maven-wrapper.properties`; -Maven 3.9 still runs on Java 8). The server stub is generated from the TES +application on Java 21, built with Maven 3.9 (through the wrapper `./mvnw`, +which downloads the version pinned in `.mvn/wrapper/maven-wrapper.properties`). The server stub is generated from the TES specification with the [Swagger Codegen Maven plugin](https://github.com/swagger-api/swagger-codegen/tree/master/modules/swagger-codegen-maven-plugin), and the [Kubernetes Java client](https://github.com/kubernetes-client/java) @@ -66,10 +65,13 @@ neither): ./mvnw spotless:check checkstyle:check ``` -The plugin versions (spotless-maven-plugin 2.30.0 with google-java-format 1.7, -Checkstyle 9.3) are the last that run on Java 8; newer ones need a newer JDK -(the move to Spring Boot 3, -[#66](https://github.com/elixir-cloud-aai/tesk-api/issues/66)). +These two commands need Java 8 (e.g. `JAVA_HOME` pointing to a Java 8 JDK), +as the CI job `Lint` runs them: google-java-format 1.7 (with +spotless-maven-plugin 2.30.0) doesn't run on Java 21, and the newer versions +that do format the code differently. The update, with the reformat it brings, +is part of the move to Spring Boot 3 +([#66](https://github.com/elixir-cloud-aai/tesk-api/issues/66)). Building and +testing need Java 21. The commits that only reformatted the code are listed in `.git-blame-ignore-revs`. GitHub skips them in its blame view; for `git blame`, diff --git a/docs/upgrading.md b/docs/upgrading.md index a4045152..2d6adde0 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -2,8 +2,19 @@ ## To the next release -From 1.2.0, clients may notice changed answers: +From 1.2.0, deployments may have to check their JVM options, and clients may +notice changed answers: +- **Java 21.** The image runs TESK API on Java 21 instead of Java 8 (see + [Deployment](deployment.md)). If you pass options to the JVM in + `JAVA_OPTS`, check that Java 21 still accepts them: options that it + removed (e.g. `-XX:MaxPermSize`, `-XX:+UseConcMarkSweepGC`) stop the JVM + at startup. Without `JAVA_OPTS`, nothing changes. +- **Inputs without a source.** An input with neither a `url` (that isn't + blank) nor a `content` (that isn't empty) is still refused with `400 Bad + Request`, but the message now names the check: + `Field 'inputs[0].urlOrContentGiven' URL or content required` instead of + `Field 'inputs[0]' URL or content required`. - **Wildcards without `path_prefix`.** An output whose `path` has wildcards (`*`, `?`, `[`) but no `path_prefix` is refused with `400 Bad Request`, unless its `url` is a `file:` URL or has no scheme, as TES 1.1 requires diff --git a/pom.xml b/pom.xml index 52be127d..81e454c1 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 2.17.1 UTF-8 UTF-8 - 1.8 + 21 3.0.0 11.0.4 @@ -293,6 +293,10 @@ ${maven-surefire-plugin.version} false + + @{argLine} --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED @@ -396,7 +400,8 @@ + (CI job Lint), on Java 8: google-java-format 1.7 does not run on Java 21, and + newer versions format differently (docs/development.md). --> com.diffplug.spotless spotless-maven-plugin From f0b99454d682d0cfd5a9b753b31a9e39b96a0804 Mon Sep 17 00:00:00 2001 From: Alex Kanitz Date: Fri, 13 Nov 2026 22:29:59 +0100 Subject: [PATCH 3/6] docs: describe how settings reach Krini Say that .env.example is the only committed env file, that all settings are read on the server at runtime, that the image holds no env files and takes no build settings, and how to pass settings to a container. Replace the stale VITE_HOST_URI note and fix the link to the deployment guide. Closes #53 Co-Authored-By: Claude Opus 5.5 --- docs/development.md | 13 +++++++--- docs/upgrading.md | 6 +++-- .../ac/ebi/tsc/tesk/tes/model/TesInput.java | 7 ++--- .../ebi/tsc/tesk/tes/model/TesInputTest.java | 26 +++++++++++++++++++ 4 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java diff --git a/docs/development.md b/docs/development.md index 722da832..66442b83 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,8 +2,8 @@ TESK API is a [Spring Boot](https://spring.io/projects/spring-boot) 2.7 application on Java 21, built with Maven 3.9 (through the wrapper `./mvnw`, -which downloads the version pinned in `.mvn/wrapper/maven-wrapper.properties`). The server stub is generated from the TES -specification with the [Swagger Codegen Maven +which downloads the version pinned in `.mvn/wrapper/maven-wrapper.properties`). +The server stub is generated from the TES specification with the [Swagger Codegen Maven plugin](https://github.com/swagger-api/swagger-codegen/tree/master/modules/swagger-codegen-maven-plugin), and the [Kubernetes Java client](https://github.com/kubernetes-client/java) talks to the Kubernetes API. @@ -71,7 +71,9 @@ spotless-maven-plugin 2.30.0) doesn't run on Java 21, and the newer versions that do format the code differently. The update, with the reformat it brings, is part of the move to Spring Boot 3 ([#66](https://github.com/elixir-cloud-aai/tesk-api/issues/66)). Building and -testing need Java 21. +testing need Java 21. Until then, write code that Java 8 can parse (no +records, text blocks, `switch` expressions or `instanceof` patterns), or the +formatter fails. The commits that only reformatted the code are listed in `.git-blame-ignore-revs`. GitHub skips them in its blame view; for `git blame`, @@ -220,7 +222,10 @@ Validation](https://beanvalidation.org/) annotations beyond those derived from the specification (e.g. `@Digits` and `@DecimalMin` on the resources of `TesResources`); restore them by hand in the generated code, and reconcile any other manual changes, e.g. the internal field `userIdFromLegacyLabel` of -`TesTaskLog` (not serialized), which the access check of a task needs. Until +`TesTaskLog` (not serialized), which the access check of a task needs, and +the check `isUrlOrContentGiven` of `TesInput` (a generator that adds +`@ScriptAssert(lang = "javascript")` instead brings back a check that fails +on Java 15 and later, which have no JavaScript engine). Until the experimental private image credential workaround remains supported, also restore the `docker_registry`, `docker_login` and `docker_pass` fields, validation, write-only handling, and experimental API descriptions in diff --git a/docs/upgrading.md b/docs/upgrading.md index 2d6adde0..263acdaa 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -8,13 +8,15 @@ notice changed answers: - **Java 21.** The image runs TESK API on Java 21 instead of Java 8 (see [Deployment](deployment.md)). If you pass options to the JVM in `JAVA_OPTS`, check that Java 21 still accepts them: options that it - removed (e.g. `-XX:MaxPermSize`, `-XX:+UseConcMarkSweepGC`) stop the JVM + removed (e.g. `-XX:MaxPermSize`, `-XX:+UseConcMarkSweepGC`, + `-XX:+UseCGroupMemoryLimitForHeap`) stop the JVM at startup. Without `JAVA_OPTS`, nothing changes. - **Inputs without a source.** An input with neither a `url` (that isn't blank) nor a `content` (that isn't empty) is still refused with `400 Bad Request`, but the message now names the check: `Field 'inputs[0].urlOrContentGiven' URL or content required` instead of - `Field 'inputs[0]' URL or content required`. + `Field 'inputs[0]' URL or content required`. The messages are meant for + people; clients should rely on the status code, not on their text. - **Wildcards without `path_prefix`.** An output whose `path` has wildcards (`*`, `?`, `[`) but no `path_prefix` is refused with `400 Bad Request`, unless its `url` is a `file:` URL or has no scheme, as TES 1.1 requires diff --git a/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java b/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java index e1f65e89..f2eebc35 100644 --- a/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java +++ b/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java @@ -191,15 +191,16 @@ public void setContent(String content) { } /** - * An input needs a source: a `url` that isn't blank, or a `content` that isn't empty. A plain - * method rather than a script (`@ScriptAssert`), as Java 15 and later have no JavaScript engine. + * An input needs a source: a `url` that isn't blank (white space as JavaScript's `trim` saw it + * before), or a `content` that isn't empty. A plain method rather than a script + * (`@ScriptAssert`), as Java 15 and later have no JavaScript engine. * * @return whether `url` or `content` is given */ @JsonIgnore @AssertTrue(message = "URL or content required") public boolean isUrlOrContentGiven() { - return (url != null && !url.trim().isEmpty()) || (content != null && !content.isEmpty()); + return (url != null && !url.isBlank()) || (content != null && !content.isEmpty()); } diff --git a/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java new file mode 100644 index 00000000..c7429ae8 --- /dev/null +++ b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java @@ -0,0 +1,26 @@ +package uk.ac.ebi.tsc.tesk.tes.model; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class TesInputTest { + + @Test + public void urlOrContentGiven() { + assertTrue(new TesInput().url("s3://bucket/file").isUrlOrContentGiven()); + assertTrue(new TesInput().content("data").isUrlOrContentGiven()); + assertTrue(new TesInput().url("s3://bucket/file").content("data").isUrlOrContentGiven()); + assertTrue(new TesInput().url(" ").content(" ").isUrlOrContentGiven()); + } + + @Test + public void neitherUrlNorContentGiven() { + assertFalse(new TesInput().isUrlOrContentGiven()); + assertFalse(new TesInput().url(" ").isUrlOrContentGiven()); + assertFalse(new TesInput().url("\u3000\t").isUrlOrContentGiven()); + assertFalse(new TesInput().content("").isUrlOrContentGiven()); + assertFalse(new TesInput().url("").content("").isUrlOrContentGiven()); + } +} From 841ffa25428148b212dab65ab195afbd36e05358 Mon Sep 17 00:00:00 2001 From: Alex Kanitz Date: Fri, 13 Nov 2026 23:21:12 +0100 Subject: [PATCH 4/6] fix: treat JS-trimmed URLs as blank Match the white space that JavaScript's trim removed (NBSP, BOM, U+2007, U+202F and the other ECMAScript WhiteSpace and LineTerminator characters) when checking an input's URL, so such URLs fail validation as before. Raised by Sourcery on #161. Co-Authored-By: Claude Opus 5.5 --- .../java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java | 13 ++++++++++++- .../uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java b/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java index f2eebc35..0e657cca 100644 --- a/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java +++ b/src/main/java/uk/ac/ebi/tsc/tesk/tes/model/TesInput.java @@ -200,7 +200,18 @@ public void setContent(String content) { @JsonIgnore @AssertTrue(message = "URL or content required") public boolean isUrlOrContentGiven() { - return (url != null && !url.isBlank()) || (content != null && !content.isEmpty()); + return (url != null && !isJavaScriptBlank(url)) || (content != null && !content.isEmpty()); + } + + /** + * Whether a string holds only white space and line terminators as JavaScript's `trim` sees + * them (ECMAScript WhiteSpace and LineTerminator). Unlike {@link String#isBlank()}, this + * includes U+00A0, U+2007, U+202F and U+FEFF, and excludes U+001C to U+001F. + */ + private static boolean isJavaScriptBlank(String s) { + return s.codePoints().allMatch(c -> c == '\t' || c == '\n' || c == 0x0B || c == '\f' + || c == '\r' || c == 0xFEFF || c == 0x2028 || c == 0x2029 + || Character.getType(c) == Character.SPACE_SEPARATOR); } diff --git a/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java index c7429ae8..39377fbb 100644 --- a/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java +++ b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java @@ -13,6 +13,8 @@ public void urlOrContentGiven() { assertTrue(new TesInput().content("data").isUrlOrContentGiven()); assertTrue(new TesInput().url("s3://bucket/file").content("data").isUrlOrContentGiven()); assertTrue(new TesInput().url(" ").content(" ").isUrlOrContentGiven()); + // JavaScript's trim keeps the information separators U+001C to U+001F. + assertTrue(new TesInput().url("\u001c").isUrlOrContentGiven()); } @Test @@ -20,6 +22,7 @@ public void neitherUrlNorContentGiven() { assertFalse(new TesInput().isUrlOrContentGiven()); assertFalse(new TesInput().url(" ").isUrlOrContentGiven()); assertFalse(new TesInput().url("\u3000\t").isUrlOrContentGiven()); + assertFalse(new TesInput().url("\u00a0\ufeff\u2007\u202f\u2028").isUrlOrContentGiven()); assertFalse(new TesInput().content("").isUrlOrContentGiven()); assertFalse(new TesInput().url("").content("").isUrlOrContentGiven()); } From 14fa65fcbb1abb887f6acd7d3564b62e10d0e8af Mon Sep 17 00:00:00 2001 From: Alex Kanitz Date: Fri, 13 Nov 2026 23:26:51 +0100 Subject: [PATCH 5/6] test: cover every JS white space branch Co-Authored-By: Claude Opus 5.5 --- src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java index 39377fbb..dd55a4ba 100644 --- a/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java +++ b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java @@ -22,7 +22,7 @@ public void neitherUrlNorContentGiven() { assertFalse(new TesInput().isUrlOrContentGiven()); assertFalse(new TesInput().url(" ").isUrlOrContentGiven()); assertFalse(new TesInput().url("\u3000\t").isUrlOrContentGiven()); - assertFalse(new TesInput().url("\u00a0\ufeff\u2007\u202f\u2028").isUrlOrContentGiven()); + assertFalse(new TesInput().url("\t\n\u000b\f\r\u00a0\ufeff\u2007\u202f\u2028\u2029").isUrlOrContentGiven()); assertFalse(new TesInput().content("").isUrlOrContentGiven()); assertFalse(new TesInput().url("").content("").isUrlOrContentGiven()); } From 5c35e322e5ded58b625d304b55452452126ccd19 Mon Sep 17 00:00:00 2001 From: Alex Kanitz Date: Fri, 13 Nov 2026 23:31:19 +0100 Subject: [PATCH 6/6] style: format the white space test Co-Authored-By: Claude Opus 5.5 --- src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java index dd55a4ba..b30769f2 100644 --- a/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java +++ b/src/test/java/uk/ac/ebi/tsc/tesk/tes/model/TesInputTest.java @@ -22,7 +22,10 @@ public void neitherUrlNorContentGiven() { assertFalse(new TesInput().isUrlOrContentGiven()); assertFalse(new TesInput().url(" ").isUrlOrContentGiven()); assertFalse(new TesInput().url("\u3000\t").isUrlOrContentGiven()); - assertFalse(new TesInput().url("\t\n\u000b\f\r\u00a0\ufeff\u2007\u202f\u2028\u2029").isUrlOrContentGiven()); + assertFalse( + new TesInput() + .url("\t\n\u000b\f\r\u00a0\ufeff\u2007\u202f\u2028\u2029") + .isUrlOrContentGiven()); assertFalse(new TesInput().content("").isUrlOrContentGiven()); assertFalse(new TesInput().url("").content("").isUrlOrContentGiven()); }