This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/release/v1.3/pr-8726-5a714329c9564a7b3f4e07f4a9c28c103cbae699
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 7da80bdbb17c8b8bc24d72ec1bcdf6f6534b1078
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Sep 27 23:59:50 2026 +0000

    fix(deploy, v1.3): load example datasets against the account's real email 
(#8726)
    
    ### What changes were proposed in this PR?
    
    Automated backport of #8723 to `release/v1.3`.
    
    Source: bcd0ec4f742264503114aa4388d82df8d35e9647 · [automation
    run](https://github.com/apache/texera/actions/runs/36359953050)
    
    ### Any related issues, documentation, discussions?
    
    Backport of #8723. Originally linked #8721.
    
    ### How was this PR tested?
    
    Release-branch CI runs on this branch once the build is fixed and this
    PR is marked ready for review.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    No.
    
    Co-authored-by: Meng Wang <[email protected]>
---
 bin/single-node/examples/load-examples.sh      |  56 ++++++-
 bin/single-node/tests/test_load_examples_sh.sh | 199 +++++++++++++++++++++++++
 2 files changed, 251 insertions(+), 4 deletions(-)

diff --git a/bin/single-node/examples/load-examples.sh 
b/bin/single-node/examples/load-examples.sh
index e6452d1152..93ad591ece 100755
--- a/bin/single-node/examples/load-examples.sh
+++ b/bin/single-node/examples/load-examples.sh
@@ -27,7 +27,15 @@ 
TEXERA_DASHBOARD_SERVICE_URL=${TEXERA_DASHBOARD_SERVICE_URL:-"http://dashboard-s
 
TEXERA_FILE_SERVICE_URL=${TEXERA_FILE_SERVICE_URL:-"http://file-service:9092/api"}
 USERNAME=${TEXERA_EXAMPLE_USERNAME:-"texera"}
 PASSWORD=${TEXERA_EXAMPLE_PASSWORD:-"texera"}
-OWNER_EMAIL="[email protected]"
+# Email this script registers the account with, on the path where it has to
+# create one. The server requires an `@`-shaped address here.
+REGISTER_EMAIL="[email protected]"
+# The account's real email, resolved from the access token in authenticate().
+# It is not derivable from $USERNAME: a freshly registered account carries
+# $REGISTER_EMAIL, while an admin seeded from USER_SYS_ADMIN_USERNAME carries
+# the username itself (AuthResource.createAdminUser). file-service looks
+# datasets up by owner email, so guessing it fails every upload (#8721).
+OWNER_EMAIL=""
 
 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
 DATASET_DIR="$SCRIPT_DIR/datasets"
@@ -36,6 +44,12 @@ WORKFLOW_DIR="$SCRIPT_DIR/workflows"
 MAX_RETRIES=60
 RETRY_INTERVAL=5
 
+# Counts print_error calls so main() can exit non-zero. Each failure below is
+# deliberately non-fatal so one bad file does not abandon the rest, but they
+# used to leave no trace at all: the loader reported success even when nothing
+# was uploaded (#8721).
+FAILURES=0
+
 # Color codes for output
 GREEN='\033[0;32m'
 RED='\033[0;31m'
@@ -47,6 +61,7 @@ print_status() {
 }
 
 print_error() {
+    FAILURES=$((FAILURES + 1))
     echo -e "${RED}[ERROR]${NC} $1"
 }
 
@@ -54,6 +69,33 @@ print_warn() {
     echo -e "${YELLOW}[WARN]${NC} $1"
 }
 
+# Read one claim out of a server-issued JWT's payload. Only the payload is
+# decoded: the token came back from our own authenticated call, so there is
+# nothing to verify here -- this just avoids guessing a value the server has
+# already told us. jq does the base64 work because the `base64` binary's decode
+# flag differs between GNU and BSD, and this script's tests run on both.
+jwt_claim() {
+    local payload="$1" claim="$2"
+    payload=$(printf '%s' "$payload" | cut -d. -f2)
+    while [ $(( ${#payload} % 4 )) -ne 0 ]; do
+        payload="${payload}="
+    done
+    printf '%s' "$payload" \
+        | jq -Rr --arg c "$claim" '(gsub("-";"+") | gsub("_";"/") | @base64d | 
fromjson)[$c] // empty'
+}
+
+# Resolve the authenticated account's email. file-service identifies a dataset
+# by (ownerEmail, datasetName), so this has to be the address the server
+# actually stored for this account.
+resolve_owner_email() {
+    OWNER_EMAIL=$(jwt_claim "$TOKEN" email 2>/dev/null || true)
+    if [ -z "$OWNER_EMAIL" ]; then
+        print_error "Could not read the 'email' claim from the access token"
+        return 1
+    fi
+    print_status "Owner email resolved as '$OWNER_EMAIL'"
+}
+
 # Wait for a service to become healthy
 wait_for_service() {
     local service_name="$1"
@@ -86,17 +128,19 @@ authenticate() {
     if echo "$LOGIN_RESPONSE" | grep -q '"accessToken"'; then
         TOKEN=$(echo "$LOGIN_RESPONSE" | grep -o '"accessToken":"[^"]*' | cut 
-d'"' -f4)
         print_status "Login successful"
+        resolve_owner_email || return 1
         return 0
     fi
 
     print_status "User doesn't exist, attempting to register..."
     REGISTER_RESPONSE=$(curl -s -X POST 
"$TEXERA_DASHBOARD_SERVICE_URL/auth/register" \
         -H "Content-Type: application/json" \
-        -d "{\"username\": \"$USERNAME\", \"email\": \"$OWNER_EMAIL\", 
\"password\": \"$PASSWORD\"}")
+        -d "{\"username\": \"$USERNAME\", \"email\": \"$REGISTER_EMAIL\", 
\"password\": \"$PASSWORD\"}")
 
     if echo "$REGISTER_RESPONSE" | grep -q '"accessToken"'; then
         TOKEN=$(echo "$REGISTER_RESPONSE" | grep -o '"accessToken":"[^"]*' | 
cut -d'"' -f4)
         print_status "Registration successful"
+        resolve_owner_email || return 1
         return 0
     fi
 
@@ -261,8 +305,7 @@ load_workflows() {
             wid=$(echo "$response" | grep -o '"wid":[0-9]*' | cut -d':' -f2)
             print_status "Workflow '$workflow_name' created with ID $wid"
         else
-            print_error "Failed to create workflow '$workflow_name'"
-            print_error "Response: $response"
+            print_error "Failed to create workflow '$workflow_name': $response"
         fi
     done
 
@@ -281,6 +324,11 @@ main() {
     load_datasets
     load_workflows
 
+    if [ "$FAILURES" -ne 0 ]; then
+        echo -e "${RED}[ERROR]${NC} === Example data loading finished with 
$FAILURES error(s) ==="
+        return 1
+    fi
+
     print_status "=== Example data loading complete ==="
 }
 
diff --git a/bin/single-node/tests/test_load_examples_sh.sh 
b/bin/single-node/tests/test_load_examples_sh.sh
new file mode 100755
index 0000000000..a79b6f2aa0
--- /dev/null
+++ b/bin/single-node/tests/test_load_examples_sh.sh
@@ -0,0 +1,199 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Tests for bin/single-node/examples/load-examples.sh. Run from the repo root:
+#   bash bin/single-node/tests/test_load_examples_sh.sh
+# Exits 0 if every check passes, 1 otherwise.
+#
+# The loader only talks to Texera over HTTP, so a stub `curl` placed ahead of
+# the real one on PATH is enough to drive every branch deterministically -- no
+# docker, no network, no sleeping. The two things guarded here both regressed
+# silently once (#8721):
+#
+#   * the ownerEmail sent to file-service must be the email the server issued
+#     in the token, not one concatenated from the username; and
+#   * a failed upload must make the loader exit non-zero instead of printing
+#     "complete".
+
+set -u
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+LOADER="$REPO_ROOT/bin/single-node/examples/load-examples.sh"
+
+PASS=0
+FAIL=0
+
+_pass() { printf "  \e[32m✓\e[0m %s\n" "$1"; PASS=$((PASS+1)); }
+_fail() {
+    printf "  \e[31m✗\e[0m %s\n" "$1"
+    [[ $# -ge 2 ]] && printf "      %s\n" "$2"
+    FAIL=$((FAIL+1))
+}
+
+# The loader itself needs jq, so a runner without it cannot exercise anything.
+if ! command -v jq >/dev/null 2>&1; then
+    _pass "skip: jq not installed — load-examples.sh cannot be exercised"
+    printf "\n%d passed, %d failed\n" "$PASS" "$FAIL"
+    exit 0
+fi
+
+# base64url, the JWT flavour: standard base64 with a different alphabet and no
+# padding. Built with jq so no `base64` binary is needed (GNU takes -d, BSD 
-D).
+_b64url() { jq -Rr '@base64' | tr '+/' '-_' | tr -d '='; }
+
+# A syntactically real JWT whose payload is $1. Never verified by anything —
+# the loader only reads claims out of a token its own login call returned.
+_mk_jwt() {
+    printf '%s.%s.%s' \
+        "$(printf '%s' '{"alg":"HS256","typ":"JWT"}' | _b64url)" \
+        "$(printf '%s' "$1" | _b64url)" \
+        "c2lnbmF0dXJl"
+}
+
+# One isolated run of the loader: a scratch tree with one dataset and one
+# workflow, a stub curl on PATH, and the loader copied in so that its
+# SCRIPT_DIR-derived datasets/ and workflows/ point at the scratch tree.
+# Behaviour knobs are read from the environment by the stub (STUB_*).
+# Called directly (never in a command substitution, which would lose the
+# variables): sets $RUN_RC, $RUN_OUT and $RUN_LOG for the caller. The login
+# token is $1; any further KEY=VAL arguments become stub knobs.
+_run_loader() {
+    local token="$1"; shift
+    local tmp
+    tmp=$(mktemp -d)
+    mkdir -p "$tmp/bin" "$tmp/examples/datasets/iris-species" 
"$tmp/examples/workflows"
+    printf 'sepal,species\n1.0,setosa\n' > 
"$tmp/examples/datasets/iris-species/Iris.csv"
+    printf 'an example\n' > 
"$tmp/examples/datasets/iris-species/description.txt"
+    printf '{"operators":[],"links":[]}\n' > "$tmp/examples/workflows/Example 
Workflow.json"
+    cp "$LOADER" "$tmp/examples/load-examples.sh"
+
+    RUN_LOG="$tmp/requests.log"
+    : > "$RUN_LOG"
+
+    cat > "$tmp/bin/curl" <<STUB
+#!/usr/bin/env bash
+# Stub curl: log the requested URL, then answer with the shape the loader
+# greps for. Only the cases the loader actually calls are handled.
+url=""
+for a in "\$@"; do case "\$a" in http*) url="\$a"; break;; esac; done
+printf '%s\n' "\$url" >> "$RUN_LOG"
+
+case "\$url" in
+    *healthcheck*)                 printf '200' ;;
+    */auth/login)
+        if [ -n "\${STUB_NO_LOGIN:-}" ]; then printf '{"error":"no such user"}'
+        else printf '{"accessToken":"%s"}' "\${STUB_LOGIN_TOKEN}"; fi ;;
+    */auth/register)               printf '{"accessToken":"%s"}' 
"\${STUB_REGISTER_TOKEN:-\${STUB_LOGIN_TOKEN}}" ;;
+    */dataset/list|*/workflow/list) printf '[]' ;;
+    */dataset/create)              printf '{"did":1}' ;;
+    *multipart-upload/part*)       printf '200' ;;
+    *type=init*)
+        if [ -n "\${STUB_FAIL_INIT:-}" ]; then printf 
'{"code":400,"message":"Dataset not found"}'
+        else printf '{"missingParts":[1],"completedPartsCount":0}'; fi ;;
+    *type=finish*)                 printf '{"message":"upload finished"}' ;;
+    *type=abort*)                  printf '{"message":"aborted"}' ;;
+    */version/create)
+        if [ -n "\${STUB_FAIL_VERSION:-}" ]; then printf '{"code":500}'
+        else printf '{"datasetVersion":{"dvid":1}}'; fi ;;
+    */workflow/create)             printf '{"wid":1}' ;;
+    *)                             printf '{}' ;;
+esac
+STUB
+    chmod +x "$tmp/bin/curl"
+
+    RUN_RC=0
+    RUN_OUT=$(PATH="$tmp/bin:$PATH" STUB_LOGIN_TOKEN="$token" \
+        env "$@" bash "$tmp/examples/load-examples.sh" 2>&1) || RUN_RC=$?
+}
+
+ADMIN_TOKEN=$(_mk_jwt 
'{"sub":"texera","userId":1,"email":"texera","role":"ADMIN"}')
+
+# 1) The owner email used for dataset lookups comes from the token, so an
+#    account whose email is not "<username>@example.com" still gets its files.
+#    This is the #8721 regression itself: every upload 400'd because the
+#    loader guessed [email protected] for an admin stored as "texera".
+_run_loader "$ADMIN_TOKEN"
+if [[ "$RUN_RC" == 0 ]]; then
+    _pass "all-success run exits 0"
+else
+    _fail "all-success run should exit 0" "rc=$RUN_RC out=$(echo "$RUN_OUT" | 
tail -3)"
+fi
+
+if grep -q 'ownerEmail=texera&' "$RUN_LOG"; then
+    _pass "ownerEmail is taken from the token's email claim"
+else
+    _fail "ownerEmail should be the token's email claim" \
+          "$(grep -o 'ownerEmail=[^&]*' "$RUN_LOG" | sort -u | tr '\n' ' ')"
+fi
+
+if grep -q '[email protected]' "$RUN_LOG"; then
+    _fail "ownerEmail must not be concatenated from the username" \
+          "$(grep -o 'ownerEmail=[^&]*' "$RUN_LOG" | sort -u | tr '\n' ' ')"
+else
+    _pass "ownerEmail is never guessed as <username>@example.com"
+fi
+
+# 2) A failed upload has to surface in the exit status. The loader keeps going
+#    so one bad file does not abandon the rest, but it must not claim success.
+_run_loader "$ADMIN_TOKEN" STUB_FAIL_INIT=1
+if [[ "$RUN_RC" != 0 ]]; then
+    _pass "failed upload makes the loader exit non-zero"
+else
+    _fail "failed upload should exit non-zero" "rc=$RUN_RC"
+fi
+if [[ "$RUN_OUT" == *"error(s)"* && "$RUN_OUT" != *"loading complete"* ]]; then
+    _pass "failed run reports the error count, not 'complete'"
+else
+    _fail "failed run should not report completion" "$(echo "$RUN_OUT" | tail 
-2)"
+fi
+
+# 3) Same for a failed version create — the other formerly-silent path.
+_run_loader "$ADMIN_TOKEN" STUB_FAIL_VERSION=1
+if [[ "$RUN_RC" != 0 ]]; then
+    _pass "failed version create makes the loader exit non-zero"
+else
+    _fail "failed version create should exit non-zero" "rc=$RUN_RC"
+fi
+
+# 4) The register fallback still sends an @-shaped address (the server rejects
+#    anything else), and the owner email then comes from the register token.
+REG_TOKEN=$(_mk_jwt 
'{"sub":"texera","userId":2,"email":"[email protected]","role":"REGULAR"}')
+_run_loader "$ADMIN_TOKEN" STUB_NO_LOGIN=1 STUB_REGISTER_TOKEN="$REG_TOKEN"
+if [[ "$RUN_RC" == 0 ]] && grep -q '/auth/register' "$RUN_LOG"; then
+    _pass "register fallback runs when login fails"
+else
+    _fail "register fallback should run and succeed" "rc=$RUN_RC out=$(echo 
"$RUN_OUT" | tail -3)"
+fi
+if grep -q 'ownerEmail=texera%40example.com\|[email protected]' 
"$RUN_LOG"; then
+    _pass "a registered account's own email is used for lookups"
+else
+    _fail "registered account should use its register email" \
+          "$(grep -o 'ownerEmail=[^&]*' "$RUN_LOG" | sort -u | tr '\n' ' ')"
+fi
+
+# 5) A token with no email claim must abort with a clear message rather than
+#    sending an empty ownerEmail and getting an opaque 400 per file.
+_run_loader "$(_mk_jwt '{"sub":"texera","userId":1,"role":"ADMIN"}')"
+if [[ "$RUN_RC" != 0 ]] && [[ "$RUN_OUT" == *"email"* ]]; then
+    _pass "token without an email claim aborts with a clear message"
+else
+    _fail "missing email claim should abort clearly" "rc=$RUN_RC out=$(echo 
"$RUN_OUT" | tail -2)"
+fi
+
+printf "\n%d passed, %d failed\n" "$PASS" "$FAIL"
+(( FAIL == 0 ))

Reply via email to