From 420aff82c4acaf232128a5d32361f9509de2ad1f Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Tue, 24 Mar 2026 13:54:43 -0400 Subject: [PATCH 01/15] feature: add core unit testing to CI --- .gitlab/stage_unit.yml | 29 +++++++++++++++++++++++++++++ core/docker/Dockerfile | 11 ++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.gitlab/stage_unit.yml b/.gitlab/stage_unit.yml index 70e4e5a98..e1536ba73 100644 --- a/.gitlab/stage_unit.yml +++ b/.gitlab/stage_unit.yml @@ -1,6 +1,7 @@ --- include: - local: .gitlab/stage_build.yml + - local: .gitlab/common.yml run-ws-unit-job: # Either the web container needs to be rebuilt or the container needs to be @@ -126,3 +127,31 @@ run-authz-unit-job: - echo "-c 'cd /datafed/source; /opt/datafed/dependencies/bin/cmake --build build --target test'" >> run_globus.sh - chmod +x run_globus.sh - ./run_globus.sh + + +run-core-unit-job: + needs: ["run-core-build-job"] + stage: unit + variables: + PROJECT: "datafed" + COMPONENT: "core" + GIT_STRATEGY: clone + INTERMEDIATE_LAYER_NAME: "build" + tags: + - ci-datafed-core + - docker + script: + - BRANCH_LOWER=$(echo "$CI_COMMIT_REF_NAME" | tr '[:upper:]' '[:lower:]') + - docker login "${REGISTRY}" -u "${HARBOR_USER}" -p "${HARBOR_DATAFED_GITLAB_CI_REGISTRY_TOKEN}" + - ./scripts/container_stop.sh -n "core-unit-" -p + - random_string=$(bash -c "cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 10 | head -n 1") + - | + docker run --rm \ + --name "core-unit-${BRANCH_LOWER}-${CI_COMMIT_SHORT_SHA}-${random_string}" \ + --entrypoint bash \ + -t "${REGISTRY}/${PROJECT}/${COMPONENT}-${INTERMEDIATE_LAYER_NAME}-${BRANCH_LOWER}:${CI_COMMIT_SHA}" \ + -c 'cd /datafed/source && \ + /opt/datafed/dependencies/bin/ctest \ + --test-dir build \ + -LE "integration|fixture" \ + --output-on-failure' diff --git a/core/docker/Dockerfile b/core/docker/Dockerfile index b1981e9ba..dde9e0e7d 100644 --- a/core/docker/Dockerfile +++ b/core/docker/Dockerfile @@ -43,6 +43,11 @@ COPY ./cmake ${BUILD_DIR}/cmake COPY ./core/docker/entrypoint.sh ${BUILD_DIR}/core/docker/ COPY ./core/server ${BUILD_DIR}/core/server +# --------------------------------------------------------------------------- +# CHANGED: Enable unit and integration tests so the intermediate image +# contains all test binaries. The production 'core' stage below still only +# copies the server binary, so tests never land in the production image. +# --------------------------------------------------------------------------- RUN ${DATAFED_DEPENDENCIES_ROOT}/scripts/generate_dependencies_config.sh && \ ${BUILD_DIR}/scripts/generate_datafed.sh && \ ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake -S. -B build \ @@ -53,10 +58,14 @@ RUN ${DATAFED_DEPENDENCIES_ROOT}/scripts/generate_dependencies_config.sh && \ -DBUILD_DOCS=False \ -DBUILD_PYTHON_CLIENT=False \ -DBUILD_FOXX=False \ - -DENABLE_INTEGRATION_TESTS=False + -DENABLE_UNIT_TESTS=True \ + -DENABLE_INTEGRATION_TESTS=True RUN ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake --build build -j 8 RUN ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake --build build --target install +# =========================================================================== +# Production image — unchanged. Only the server binary is copied in. +# =========================================================================== FROM ${RUNTIME} AS core SHELL ["/bin/bash", "-c"] From 0cead873ed1b4efd169837f0867d9b93b2c867e5 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Tue, 24 Mar 2026 16:51:29 -0400 Subject: [PATCH 02/15] refactor: leave off integration tests for first step. --- core/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/docker/Dockerfile b/core/docker/Dockerfile index dde9e0e7d..eb1ebc36e 100644 --- a/core/docker/Dockerfile +++ b/core/docker/Dockerfile @@ -59,7 +59,7 @@ RUN ${DATAFED_DEPENDENCIES_ROOT}/scripts/generate_dependencies_config.sh && \ -DBUILD_PYTHON_CLIENT=False \ -DBUILD_FOXX=False \ -DENABLE_UNIT_TESTS=True \ - -DENABLE_INTEGRATION_TESTS=True + -DENABLE_INTEGRATION_TESTS=False RUN ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake --build build -j 8 RUN ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake --build build --target install From 3096a7a5ee39b228c17307bef022d900b491b7d2 Mon Sep 17 00:00:00 2001 From: Joshua S Brown Date: Wed, 25 Mar 2026 06:20:23 -0400 Subject: [PATCH 03/15] Apply suggestions from code review Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .gitlab/stage_unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/stage_unit.yml b/.gitlab/stage_unit.yml index e1536ba73..8a246e63e 100644 --- a/.gitlab/stage_unit.yml +++ b/.gitlab/stage_unit.yml @@ -142,7 +142,7 @@ run-core-unit-job: - docker script: - BRANCH_LOWER=$(echo "$CI_COMMIT_REF_NAME" | tr '[:upper:]' '[:lower:]') - - docker login "${REGISTRY}" -u "${HARBOR_USER}" -p "${HARBOR_DATAFED_GITLAB_CI_REGISTRY_TOKEN}" + - echo "${HARBOR_DATAFED_GITLAB_CI_REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${HARBOR_USER}" --password-stdin - ./scripts/container_stop.sh -n "core-unit-" -p - random_string=$(bash -c "cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 10 | head -n 1") - | From 6be23f4200bb1c1da3c13b741522cfc896fdddb5 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 25 Mar 2026 06:26:01 -0400 Subject: [PATCH 04/15] fix: make sure build container in intermediate stage exists. --- .gitlab/build/force_build_core_image.yml | 4 +++- .gitlab/build/retag_core_image.yml | 4 +++- .gitlab/stage_unit.yml | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitlab/build/force_build_core_image.yml b/.gitlab/build/force_build_core_image.yml index 1520cd505..cd0ad710b 100644 --- a/.gitlab/build/force_build_core_image.yml +++ b/.gitlab/build/force_build_core_image.yml @@ -14,7 +14,9 @@ build-core: GIT_STRATEGY: clone DOCKER_FILE_PATH: "core/docker/Dockerfile" DATAFED_HARBOR_REGISTRY: "$REGISTRY" # needed by c_harbor_artifact_count - BUILD_INTERMEDIATE: "FALSE" + BUILD_INTERMEDIATE: "TRUE" + INTERMEDIATE_TARGET: "core-build" # Name of the layer in the dockerfile + INTERMEDIATE_LAYER_NAME: "build" tags: - ci-datafed-core - docker diff --git a/.gitlab/build/retag_core_image.yml b/.gitlab/build/retag_core_image.yml index 8b719df82..6042b367b 100644 --- a/.gitlab/build/retag_core_image.yml +++ b/.gitlab/build/retag_core_image.yml @@ -13,6 +13,8 @@ retag-image: COMPONENT: "core" GIT_STRATEGY: clone DATAFED_HARBOR_REGISTRY: "$REGISTRY" # needed by c_harbor_artifact_count - BUILD_INTERMEDIATE: "FALSE" + BUILD_INTERMEDIATE: "TRUE" + INTERMEDIATE_TARGET: "core-build" # Name of the layer in the dockerfile + INTERMEDIATE_LAYER_NAME: "build" tags: - docker diff --git a/.gitlab/stage_unit.yml b/.gitlab/stage_unit.yml index 8a246e63e..bd14d9e33 100644 --- a/.gitlab/stage_unit.yml +++ b/.gitlab/stage_unit.yml @@ -128,7 +128,6 @@ run-authz-unit-job: - chmod +x run_globus.sh - ./run_globus.sh - run-core-unit-job: needs: ["run-core-build-job"] stage: unit From 24b056954e5dec3e2f224b16159336fb0530ef87 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 25 Mar 2026 06:36:31 -0400 Subject: [PATCH 05/15] feat: enable building of core integration tests --- core/docker/Dockerfile | 2 +- tests/mock/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/docker/Dockerfile b/core/docker/Dockerfile index eb1ebc36e..dde9e0e7d 100644 --- a/core/docker/Dockerfile +++ b/core/docker/Dockerfile @@ -59,7 +59,7 @@ RUN ${DATAFED_DEPENDENCIES_ROOT}/scripts/generate_dependencies_config.sh && \ -DBUILD_PYTHON_CLIENT=False \ -DBUILD_FOXX=False \ -DENABLE_UNIT_TESTS=True \ - -DENABLE_INTEGRATION_TESTS=False + -DENABLE_INTEGRATION_TESTS=True RUN ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake --build build -j 8 RUN ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake --build build --target install diff --git a/tests/mock/CMakeLists.txt b/tests/mock/CMakeLists.txt index 0e8b7eeb0..759f52d49 100644 --- a/tests/mock/CMakeLists.txt +++ b/tests/mock/CMakeLists.txt @@ -8,7 +8,7 @@ foreach(PROG file(GLOB ${PROG}_SOURCES ${PROG}*.cpp) add_executable(mock_liveness_${PROG} ${${PROG}_SOURCES}) - target_link_libraries(mock_liveness_${PROG} PUBLIC ${DATAFED_BOOST_LIBRARIES} ${DATAFED_GSSAPI_LIBRARIES} ${DATAFED_GLOBUS_COMMON_LIBRARIES} common ) + target_link_libraries(mock_liveness_${PROG} PUBLIC ${DATAFED_BOOST_LIBRARIES} common ) if(BUILD_SHARED_LIBS) target_compile_definitions(mock_liveness_${PROG} PRIVATE BOOST_TEST_DYN_LINK) endif() From 51ced0ca67b6584287e0364f0ffc5c7816c8d3ec Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 25 Mar 2026 07:01:44 -0400 Subject: [PATCH 06/15] fix: add intermediate to core image check. --- .gitlab/stage_image_check.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab/stage_image_check.yml b/.gitlab/stage_image_check.yml index d29b8209f..5cf061748 100644 --- a/.gitlab/stage_image_check.yml +++ b/.gitlab/stage_image_check.yml @@ -18,7 +18,8 @@ check-core-image: variables: PROJECT: "datafed" COMPONENT: "core" - BUILD_INTERMEDIATE: "FALSE" + BUILD_INTERMEDIATE: "TRUE" + INTERMEDIATE_LAYER_NAME: "build" WATCHED_PATHS: "docker scripts core common CMakeLists.txt cmake .gitlab-ci.yml" check-repo-image: From b20add2ad6ff1823dde2bdeedb38ef9b5450e41c Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 25 Mar 2026 07:23:08 -0400 Subject: [PATCH 07/15] fix: add missing test labels --- tests/CMakeLists.txt | 4 ++-- tests/mock/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1677976af..cf8f01d48 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,8 +27,8 @@ if( ENABLE_INTEGRATION_TESTS ) WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/tests/mock_core ) - set_tests_properties(start_mock PROPERTIES FIXTURES_SETUP FIX_MOCK) - set_tests_properties(stop_mock PROPERTIES FIXTURES_CLEANUP FIX_MOCK) + set_tests_properties(start_mock PROPERTIES FIXTURES_SETUP FIX_MOCK LABELS "integration") + set_tests_properties(stop_mock PROPERTIES FIXTURES_CLEANUP FIX_MOCK LABELS "integration")) add_subdirectory(mock_core) add_subdirectory(mock) diff --git a/tests/mock/CMakeLists.txt b/tests/mock/CMakeLists.txt index 0e8b7eeb0..038f16943 100644 --- a/tests/mock/CMakeLists.txt +++ b/tests/mock/CMakeLists.txt @@ -19,6 +19,6 @@ foreach(PROG COMMAND ${CMAKE_CURRENT_BINARY_DIR}/mock_liveness_${PROG} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/tests/mock_core ) - set_tests_properties(mock_liveness_${PROG} PROPERTIES FIXTURES_REQUIRED FIX_MOCK) + set_tests_properties(mock_liveness_${PROG} PROPERTIES FIXTURES_REQUIRED FIX_MOCK LABELS "integration") endforeach(PROG) From 2cc37f93062cbdad46f79f48cebcf8e9a76137d7 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 25 Mar 2026 07:24:28 -0400 Subject: [PATCH 08/15] refactor: add core build intermediate stage to compose build --- scripts/compose_build_images.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/compose_build_images.sh b/scripts/compose_build_images.sh index 1536367b7..ee771de5f 100755 --- a/scripts/compose_build_images.sh +++ b/scripts/compose_build_images.sh @@ -73,6 +73,13 @@ if [[ "$BUILD_METADATA" == "TRUE" ]]; then --build-arg BASE_IMAGE=$BASE_IMAGE \ -t datafed-runtime:latest fi + docker build -f \ + "${PROJECT_ROOT}/core/docker/Dockerfile" \ + --build-arg DEPENDENCIES="datafed-dependencies:latest" \ + --build-arg RUNTIME="datafed-runtime:latest" \ + --target core-build \ + "${PROJECT_ROOT}" \ + -t datafed-core-build:latest docker build -f \ "${PROJECT_ROOT}/core/docker/Dockerfile" \ --build-arg DEPENDENCIES="datafed-dependencies:latest" \ From 7cf7dcae5951ec55c75862db4e71cdf4211105f1 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 25 Mar 2026 14:35:51 -0400 Subject: [PATCH 09/15] refactor: Update CMakeLists.txt to include client handler for schemas. --- core/server/tests/integration/CMakeLists.txt | 80 +++++++++++++++++++- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/core/server/tests/integration/CMakeLists.txt b/core/server/tests/integration/CMakeLists.txt index e26556b19..f46bc9315 100644 --- a/core/server/tests/integration/CMakeLists.txt +++ b/core/server/tests/integration/CMakeLists.txt @@ -11,11 +11,20 @@ set(MOCK_SCHEMA_PORT "4010" set(MOCK_SCHEMA_PULL "true" CACHE STRING "Pull the mock image before starting (set to 'false' for offline/local builds)") +set(ARANGO_IMAGE "savannah.ornl.gov/datafed/arango-foxx:latest" + CACHE STRING "Docker image for the ArangoDB test instance (with Foxx services)") +set(ARANGO_PORT "8529" + CACHE STRING "Host port for the ArangoDB test container") +set(ARANGO_ROOT_PASS "test" + CACHE STRING "Root password for the ArangoDB test instance") +set(ARANGO_PULL "true" + CACHE STRING "Pull the ArangoDB image before starting (set to 'false' for offline/local builds)") + # ── Check for Docker (needed by SchemaAPIClient tests) ─────────────────────── find_program(DOCKER_EXECUTABLE docker) if(NOT DOCKER_EXECUTABLE) - message(STATUS "Docker not found — SchemaAPIClient integration tests will be skipped") + message(STATUS "Docker not found — integration tests requiring containers will be skipped") endif() # ── Compile and register all integration tests ────────────────────────────── @@ -23,6 +32,7 @@ endif() foreach(PROG test_SchemaServiceFactory test_SchemaAPIClient + test_SchemaHandler ) file(GLOB ${PROG}_SOURCES ${PROG}*.cpp) add_executable(integration_${PROG} ${${PROG}_SOURCES}) @@ -52,6 +62,10 @@ endforeach(PROG) if(DOCKER_EXECUTABLE) + # ======================================================================== + # Mock Schema API (Prism) fixture — used by SchemaAPIClient + # ======================================================================== + set(_mock_schema_env "MOCK_SCHEMA_IMAGE=${MOCK_SCHEMA_IMAGE}" "MOCK_SCHEMA_PORT=${MOCK_SCHEMA_PORT}" @@ -59,7 +73,7 @@ if(DOCKER_EXECUTABLE) "CONTAINER_NAME=datafed-mock-schema" ) - set(_test_env + set(_mock_schema_test_env "SCHEMA_API_TEST_URL=http://localhost:${MOCK_SCHEMA_PORT}" ) @@ -91,14 +105,72 @@ if(DOCKER_EXECUTABLE) set_tests_properties(integration_test_SchemaAPIClient PROPERTIES FIXTURES_REQUIRED MockSchemaAPI - ENVIRONMENT "${_test_env}" + ENVIRONMENT "${_mock_schema_test_env}" LABELS "integration" ) if(ENABLE_MEMORY_TESTS) set_tests_properties(memory_test_SchemaAPIClient PROPERTIES FIXTURES_REQUIRED MockSchemaAPI - ENVIRONMENT "${_test_env}" + ENVIRONMENT "${_mock_schema_test_env}" + LABELS "integration;memory" + ) + endif() + + # ======================================================================== + # ArangoDB fixture — used by SchemaHandler + # ======================================================================== + + set(_arango_env + "ARANGO_IMAGE=${ARANGO_IMAGE}" + "ARANGO_PORT=${ARANGO_PORT}" + "ARANGO_ROOT_PASS=${ARANGO_ROOT_PASS}" + "ARANGO_PULL=${ARANGO_PULL}" + "ARANGO_CONTAINER=datafed-test-arango" + ) + + set(_arango_test_env + "DATAFED_TEST_ARANGO_URL=http://localhost:${ARANGO_PORT}" + "DATAFED_TEST_ARANGO_USER=root" + "DATAFED_TEST_ARANGO_PASS=${ARANGO_ROOT_PASS}" + ) + + # ── Fixture setup: start ArangoDB container ────────────────────────── + + add_test( + NAME arango_start + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/start_test_arango.sh + ) + set_tests_properties(arango_start PROPERTIES + FIXTURES_SETUP TestArangoDB + ENVIRONMENT "${_arango_env}" + LABELS "fixture;integration" + ) + + # ── Fixture cleanup: stop and remove container ─────────────────────── + + add_test( + NAME arango_stop + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/stop_test_arango.sh + ) + set_tests_properties(arango_stop PROPERTIES + FIXTURES_CLEANUP TestArangoDB + ENVIRONMENT "${_arango_env}" + LABELS "fixture;integration" + ) + + # ── Attach fixture to SchemaHandler tests ──────────────────────────── + + set_tests_properties(integration_test_SchemaHandler PROPERTIES + FIXTURES_REQUIRED TestArangoDB + ENVIRONMENT "${_arango_test_env}" + LABELS "integration" + ) + + if(ENABLE_MEMORY_TESTS) + set_tests_properties(memory_test_SchemaHandler PROPERTIES + FIXTURES_REQUIRED TestArangoDB + ENVIRONMENT "${_arango_test_env}" LABELS "integration;memory" ) endif() From 65693fe68143445a741cf6ca701ad05de314ad2a Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Fri, 27 Mar 2026 11:54:52 -0400 Subject: [PATCH 10/15] fix: get arango working with core server client handler integration tests. --- core/server/tests/integration/CMakeLists.txt | 16 +- .../tests/integration/start_test_arango.sh | 99 +++ .../tests/integration/stop_test_arango.sh | 26 + .../tests/integration/test_SchemaHandler.cpp | 751 ++++++++++++++++++ 4 files changed, 885 insertions(+), 7 deletions(-) create mode 100755 core/server/tests/integration/start_test_arango.sh create mode 100755 core/server/tests/integration/stop_test_arango.sh create mode 100644 core/server/tests/integration/test_SchemaHandler.cpp diff --git a/core/server/tests/integration/CMakeLists.txt b/core/server/tests/integration/CMakeLists.txt index f46bc9315..43f68c839 100644 --- a/core/server/tests/integration/CMakeLists.txt +++ b/core/server/tests/integration/CMakeLists.txt @@ -122,17 +122,19 @@ if(DOCKER_EXECUTABLE) # ======================================================================== set(_arango_env - "ARANGO_IMAGE=${ARANGO_IMAGE}" - "ARANGO_PORT=${ARANGO_PORT}" - "ARANGO_ROOT_PASS=${ARANGO_ROOT_PASS}" - "ARANGO_PULL=${ARANGO_PULL}" + "ARANGO_IMAGE=${ARANGO_TEST_IMAGE}" + "ARANGO_PORT=${ARANGO_TEST_PORT}" + "ARANGO_ROOT_PASS=${ARANGO_TEST_ROOT_PASS}" "ARANGO_CONTAINER=datafed-test-arango" + "ARANGO_PULL=${ARANGO_TEST_PULL}" + "DATAFED_PROJECT_ROOT=${DataFed_SOURCE_DIR}" + "DATAFED_DATABASE_NAME=${DATAFED_TEST_DATABASE_NAME}" ) set(_arango_test_env - "DATAFED_TEST_ARANGO_URL=http://localhost:${ARANGO_PORT}" - "DATAFED_TEST_ARANGO_USER=root" - "DATAFED_TEST_ARANGO_PASS=${ARANGO_ROOT_PASS}" + "ARANGO_TEST_URL=http://localhost:${ARANGO_TEST_PORT}" + "ARANGO_TEST_ROOT_PASS=${ARANGO_TEST_ROOT_PASS}" + "DATAFED_DATABASE_NAME=${ARANGO_TEST_DATABASE_NAME}" ) # ── Fixture setup: start ArangoDB container ────────────────────────── diff --git a/core/server/tests/integration/start_test_arango.sh b/core/server/tests/integration/start_test_arango.sh new file mode 100755 index 000000000..b617addf3 --- /dev/null +++ b/core/server/tests/integration/start_test_arango.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# start_test_arango.sh — Start (or reuse) ArangoDB and provision Foxx services. +# +# If ArangoDB is already reachable at the target address, the existing instance +# is reused and no container is started. Either way, Foxx services are +# (re)installed via scripts/install_foxx.sh. +# +# Required env: +# DATAFED_PROJECT_ROOT — Path to the DataFed repository root +# ARANGO_IMAGE — Docker image (only required when starting a container) +# +# Optional env: +# ARANGO_PORT — Host port to bind (default: 8529) +# ARANGO_ROOT_PASS — Root password (default: "test") +# ARANGO_CONTAINER — Container name (default: datafed-test-arango) +# ARANGO_PULL — "true" to pull image before starting (default: true) +set -eu + +PROJECT_ROOT="${DATAFED_PROJECT_ROOT:?DATAFED_PROJECT_ROOT must be set}" +PORT="${ARANGO_PORT:-8529}" +ROOT_PASS="${ARANGO_ROOT_PASS:-test}" +NAME="${ARANGO_CONTAINER:-datafed-test-arango}" +PULL="${ARANGO_PULL:-true}" +MAX_WAIT=60 + +# State file: if we create it, the cleanup step knows to tear down the +# container. If ArangoDB was already running, the file is never created +# and cleanup leaves the instance alone. +STATE_FILE="/tmp/${NAME}.started-by-fixture" +rm -f "${STATE_FILE}" + +# ── Check for an already-running instance ──────────────────────────────────── + +if curl -sf -o /dev/null \ + -u "root:${ROOT_PASS}" \ + "http://localhost:${PORT}/_api/version" 2>/dev/null; then + echo "ArangoDB already reachable on port ${PORT} — reusing existing instance" +else + # ── No instance found — start a container ──────────────────────────────── + IMAGE="${ARANGO_IMAGE:?ARANGO_IMAGE must be set (no running ArangoDB found)}" + + if ! command -v docker &>/dev/null; then + echo "ERROR: docker not found and no ArangoDB running on port ${PORT}" >&2 + exit 1 + fi + + docker rm -f "${NAME}" &>/dev/null || true + + if [ "${PULL}" = "true" ]; then + echo "Pulling ${IMAGE}..." + docker pull "${IMAGE}" + fi + + echo "Starting ArangoDB test instance on port ${PORT}..." + docker run -d \ + --name "${NAME}" \ + -p "${PORT}:8529" \ + -e ARANGO_ROOT_PASSWORD="${ROOT_PASS}" \ + "${IMAGE}" + + # ── Wait for readiness ─────────────────────────────────────────────────── + echo "Waiting for ArangoDB readiness (max ${MAX_WAIT}s)..." + for i in $(seq 1 "${MAX_WAIT}"); do + if curl -sf -o /dev/null \ + -u "root:${ROOT_PASS}" \ + "http://localhost:${PORT}/_api/version" 2>/dev/null; then + echo "ArangoDB ready on port ${PORT} (took ${i}s)" + break + fi + if [ "$i" -eq "${MAX_WAIT}" ]; then + echo "ERROR: ArangoDB did not become ready in ${MAX_WAIT}s" >&2 + echo "--- container logs ---" + docker logs "${NAME}" 2>&1 | tail -50 || true + echo "--- end logs ---" + docker rm -f "${NAME}" &>/dev/null || true + exit 1 + fi + sleep 1 + done + + # Mark that we started the container so cleanup knows to remove it. + touch "${STATE_FILE}" +fi + +# ── Provision Foxx services ────────────────────────────────────────────────── +# +# install_foxx.sh is idempotent — it checks whether the DB/services exist and +# creates or replaces as needed. We pass credentials via env + CLI flags so +# the script finds the instance we just validated above. +echo "Provisioning database and Foxx services..." +export DATAFED_DATABASE_PASSWORD="${ROOT_PASS}" +export DATAFED_DATABASE_HOST="localhost" + +bash "${DATAFED_PROJECT_ROOT}/scripts/install_foxx.sh" \ + -p "${ROOT_PASS}" \ + -u "root" \ + -i "localhost" + +echo "Foxx provisioning complete." diff --git a/core/server/tests/integration/stop_test_arango.sh b/core/server/tests/integration/stop_test_arango.sh new file mode 100755 index 000000000..0b55ef910 --- /dev/null +++ b/core/server/tests/integration/stop_test_arango.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# stop_test_arango.sh — Stop and remove the ArangoDB test container, but only +# if the fixture started it. +# +# If start_test_arango.sh reused a pre-existing instance, the state file won't +# exist and this script is a no-op. +# +# Optional env: +# ARANGO_CONTAINER — Container name (default: datafed-test-arango) +set -u + +NAME="${ARANGO_CONTAINER:-datafed-test-arango}" +STATE_FILE="/tmp/${NAME}.started-by-fixture" + +if [ -f "${STATE_FILE}" ]; then + if docker rm -f "${NAME}" &>/dev/null; then + echo "Stopped and removed container: ${NAME}" + else + echo "Container ${NAME} was not running (already removed?)" + fi + rm -f "${STATE_FILE}" +else + echo "ArangoDB was pre-existing — leaving it alone" +fi + +exit 0 diff --git a/core/server/tests/integration/test_SchemaHandler.cpp b/core/server/tests/integration/test_SchemaHandler.cpp new file mode 100644 index 000000000..fb888ca08 --- /dev/null +++ b/core/server/tests/integration/test_SchemaHandler.cpp @@ -0,0 +1,751 @@ +#define BOOST_TEST_MAIN +#define BOOST_TEST_MODULE SchemaHandlerIntegration +#include + +// Local includes +#include "client_handlers/SchemaHandler.hpp" +#include "DatabaseAPI.hpp" +#include "common/TraceException.hpp" + +// Standard includes +#include +#include +#include +#include + +using namespace SDMS::Core; +using namespace SDMS; + +// ============================================================================ +// Fixture +// +// Connects to a live ArangoDB instance with Foxx services installed. +// +// Required environment variables: +// DATAFED_TEST_ARANGO_URL — e.g. "http://localhost:8529" +// DATAFED_TEST_ARANGO_USER — e.g. "root" +// DATAFED_TEST_ARANGO_PASS — password +// +// Optional: +// DATAFED_TEST_ARANGO_DB — database name (default: "sdms") +// +// If env vars are missing, all tests in this file are skipped (not failed). +// +// Each test case uses the fixture's createTestSchema / cleanup helpers +// to avoid test interdependencies. +// ============================================================================ + +namespace { + +const std::string TEST_USER = "u/integration_test_user"; + +const std::string VALID_SCHEMA_DEF = R"({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": { "type": "number" }, + "tags": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["name", "value"] +})"; + +const std::string UPDATED_SCHEMA_DEF = R"({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "value": { "type": "number" }, + "unit": { "type": "string" }, + "tags": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["name", "value"] +})"; + +const std::string VALID_METADATA = R"({ + "name": "widget", + "value": 42.0, + "tags": ["alpha", "beta"] +})"; + +const std::string INVALID_METADATA_MISSING_REQUIRED = R"({ + "value": 42.0 +})"; + +const std::string INVALID_METADATA_WRONG_TYPE = R"({ + "name": "widget", + "value": "not_a_number" +})"; + +const std::string INVALID_JSON = R"({not valid json at all)"; + +const std::string SCHEMA_MISSING_TYPE = R"({ + "properties": { "x": { "type": "string" } } +})"; + +const std::string SCHEMA_MISSING_PROPERTIES = R"({ "type": "object" })"; + +} // anonymous namespace + +struct ArangoFixture { + std::unique_ptr db; + std::unique_ptr handler; + LogContext log_context; + bool available; + + // Track created schema IDs for cleanup + std::vector created_schemas; + + ArangoFixture() : available(false) { + std::string url = getEnv("DATAFED_TEST_ARANGO_URL"); + std::string user = getEnv("DATAFED_TEST_ARANGO_USER"); + std::string pass = getEnv("DATAFED_TEST_ARANGO_PASS"); + + if (url.empty() || user.empty()) { + return; + } + + log_context.thread_name = "schema-handler-integration"; + log_context.thread_id = 0; + log_context.correlation_id = "schema-handler-test"; + + try { + db = std::make_unique(url, user, pass); + handler = std::make_unique(*db); + available = true; + } catch (std::exception &e) { + BOOST_TEST_MESSAGE("ArangoDB connection failed: " << e.what()); + } + } + + ~ArangoFixture() { cleanup(); } + + void cleanup() { + if (!available || !handler) + return; + + for (auto it = created_schemas.rbegin(); it != created_schemas.rend(); + ++it) { + try { + SchemaDeleteRequest req; + AckReply reply; + req.set_id(*it); + handler->handleDelete(TEST_USER, req, reply, log_context); + } catch (...) { + // Best-effort cleanup — don't mask test failures + } + } + created_schemas.clear(); + } + + /// Create a schema and track it for cleanup. Returns the schema ID. + std::string createTestSchema(const std::string &name, + const std::string &def = VALID_SCHEMA_DEF, + const std::string &desc = "integration test") { + SchemaCreateRequest req; + SchemaDataReply reply; + + req.set_id(name); + req.set_def(def); + req.set_desc(desc); + req.set_pub(false); + req.set_sys(false); + req.set_type("json-schema"); + req.set_format("json"); + + handler->handleCreate(TEST_USER, req, reply, log_context); + + std::string id = reply.schema(0).id(); + created_schemas.push_back(id); + return id; + } + + bool isAvailable() const { return available; } + + static std::string getEnv(const char *name) { + const char *val = std::getenv(name); + if (!val || std::string(val).empty()) + return ""; + return val; + } +}; + +/// Skip macro — avoids nesting every test body in an if block. +#define SKIP_IF_UNAVAILABLE() \ + if (!isAvailable()) { \ + BOOST_TEST_MESSAGE("Skipping: test ArangoDB not configured"); \ + return; \ + } + +// ============================================================================ +// Test Suite: handleCreate +// ============================================================================ + +BOOST_FIXTURE_TEST_SUITE(HandleCreate, ArangoFixture) + +BOOST_AUTO_TEST_CASE(create_valid_schema) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_create_valid"); + + // ID should be versioned: name:version + BOOST_TEST(id.find("test_create_valid:") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(create_and_view_round_trip) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_create_view", VALID_SCHEMA_DEF, + "round trip test"); + + // View it back + SchemaViewRequest view_req; + SchemaDataReply view_reply; + view_req.set_id(id); + + handler->handleView(TEST_USER, view_req, view_reply, log_context); + + BOOST_REQUIRE(view_reply.schema_size() > 0); + BOOST_TEST(view_reply.schema(0).id() == id); + BOOST_TEST(view_reply.schema(0).desc() == "round trip test"); + + // def should contain the schema definition + std::string returned_def = view_reply.schema(0).def(); + BOOST_TEST(!returned_def.empty()); + + // Parse and verify structure + nlohmann::json parsed = nlohmann::json::parse(returned_def); + BOOST_TEST(parsed.contains("properties")); + BOOST_TEST(parsed["type"].get() == "object"); +} + +BOOST_AUTO_TEST_CASE(create_with_invalid_json_throws) { + SKIP_IF_UNAVAILABLE(); + + SchemaCreateRequest req; + SchemaDataReply reply; + + req.set_id("test_bad_json"); + req.set_def(INVALID_JSON); + req.set_desc("should fail"); + req.set_pub(false); + req.set_sys(false); + req.set_type("json-schema"); + req.set_format("json"); + + BOOST_CHECK_THROW( + handler->handleCreate(TEST_USER, req, reply, log_context), + TraceException); +} + +BOOST_AUTO_TEST_CASE(create_with_missing_properties_throws) { + SKIP_IF_UNAVAILABLE(); + + SchemaCreateRequest req; + SchemaDataReply reply; + + req.set_id("test_no_props"); + req.set_def(SCHEMA_MISSING_PROPERTIES); + req.set_desc("should fail"); + req.set_pub(false); + req.set_sys(false); + req.set_type("json-schema"); + req.set_format("json"); + + BOOST_CHECK_THROW( + handler->handleCreate(TEST_USER, req, reply, log_context), + TraceException); +} + +BOOST_AUTO_TEST_CASE(create_with_missing_type_throws) { + SKIP_IF_UNAVAILABLE(); + + SchemaCreateRequest req; + SchemaDataReply reply; + + req.set_id("test_no_type"); + req.set_def(SCHEMA_MISSING_TYPE); + req.set_desc("should fail"); + req.set_pub(false); + req.set_sys(false); + req.set_type("json-schema"); + req.set_format("json"); + + BOOST_CHECK_THROW( + handler->handleCreate(TEST_USER, req, reply, log_context), + TraceException); +} + +BOOST_AUTO_TEST_CASE(create_with_description) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_create_desc", VALID_SCHEMA_DEF, + "detailed description here"); + + SchemaViewRequest view_req; + SchemaDataReply view_reply; + view_req.set_id(id); + + handler->handleView(TEST_USER, view_req, view_reply, log_context); + + BOOST_REQUIRE(view_reply.schema_size() > 0); + BOOST_TEST(view_reply.schema(0).desc() == "detailed description here"); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ============================================================================ +// Test Suite: handleRevise +// ============================================================================ + +BOOST_FIXTURE_TEST_SUITE(HandleRevise, ArangoFixture) + +BOOST_AUTO_TEST_CASE(revise_creates_new_version) { + SKIP_IF_UNAVAILABLE(); + + std::string id_v1 = createTestSchema("test_revise"); + + // Revise with updated definition + SchemaReviseRequest rev_req; + SchemaDataReply rev_reply; + rev_req.set_id(id_v1); + rev_req.set_def(UPDATED_SCHEMA_DEF); + rev_req.set_desc("revised version"); + + handler->handleRevise(TEST_USER, rev_req, rev_reply, log_context); + + BOOST_REQUIRE(rev_reply.schema_size() > 0); + std::string id_v2 = rev_reply.schema(0).id(); + created_schemas.push_back(id_v2); + + // New ID should differ from original + BOOST_TEST(id_v2 != id_v1); + + // View the new revision + SchemaViewRequest view_req; + SchemaDataReply view_reply; + view_req.set_id(id_v2); + + handler->handleView(TEST_USER, view_req, view_reply, log_context); + + BOOST_REQUIRE(view_reply.schema_size() > 0); + BOOST_TEST(view_reply.schema(0).desc() == "revised version"); + + // Verify the new definition has the added field + nlohmann::json parsed = + nlohmann::json::parse(view_reply.schema(0).def()); + BOOST_TEST(parsed["properties"].contains("unit")); +} + +BOOST_AUTO_TEST_CASE(revise_without_def_skips_validation) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_revise_no_def"); + + // Revise with no def — should not throw from validation + SchemaReviseRequest rev_req; + SchemaDataReply rev_reply; + rev_req.set_id(id); + // Deliberately NOT setting def + + // May throw from DB (revision logic) but must NOT throw from validation + try { + handler->handleRevise(TEST_USER, rev_req, rev_reply, log_context); + if (rev_reply.schema_size() > 0) { + created_schemas.push_back(rev_reply.schema(0).id()); + } + } catch (TraceException &e) { + std::string msg = e.toString(); + BOOST_TEST(msg.find("Invalid metadata schema") == std::string::npos); + BOOST_TEST(msg.find("Schema validation failed") == std::string::npos); + } +} + +BOOST_AUTO_TEST_CASE(revise_with_invalid_def_throws) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_revise_bad_def"); + + SchemaReviseRequest rev_req; + SchemaDataReply rev_reply; + rev_req.set_id(id); + rev_req.set_def(INVALID_JSON); + + BOOST_CHECK_THROW( + handler->handleRevise(TEST_USER, rev_req, rev_reply, log_context), + TraceException); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ============================================================================ +// Test Suite: handleUpdate +// ============================================================================ + +BOOST_FIXTURE_TEST_SUITE(HandleUpdate, ArangoFixture) + +BOOST_AUTO_TEST_CASE(update_description_only) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_update_desc", VALID_SCHEMA_DEF, + "original description"); + + SchemaUpdateRequest upd_req; + SchemaDataReply upd_reply; + upd_req.set_id(id); + upd_req.set_desc("updated description"); + + handler->handleUpdate(TEST_USER, upd_req, upd_reply, log_context); + + // View and verify + SchemaViewRequest view_req; + SchemaDataReply view_reply; + view_req.set_id(id); + handler->handleView(TEST_USER, view_req, view_reply, log_context); + + BOOST_REQUIRE(view_reply.schema_size() > 0); + BOOST_TEST(view_reply.schema(0).desc() == "updated description"); +} + +BOOST_AUTO_TEST_CASE(update_definition) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_update_def"); + + SchemaUpdateRequest upd_req; + SchemaDataReply upd_reply; + upd_req.set_id(id); + upd_req.set_def(UPDATED_SCHEMA_DEF); + + handler->handleUpdate(TEST_USER, upd_req, upd_reply, log_context); + + // View and verify the new def + SchemaViewRequest view_req; + SchemaDataReply view_reply; + view_req.set_id(id); + handler->handleView(TEST_USER, view_req, view_reply, log_context); + + BOOST_REQUIRE(view_reply.schema_size() > 0); + nlohmann::json parsed = + nlohmann::json::parse(view_reply.schema(0).def()); + BOOST_TEST(parsed["properties"].contains("unit")); +} + +BOOST_AUTO_TEST_CASE(update_without_def_skips_validation) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_update_no_def"); + + SchemaUpdateRequest upd_req; + SchemaDataReply upd_reply; + upd_req.set_id(id); + // Deliberately NOT setting def — only description + upd_req.set_desc("description only update"); + + // Must NOT throw from validation + try { + handler->handleUpdate(TEST_USER, upd_req, upd_reply, log_context); + } catch (TraceException &e) { + std::string msg = e.toString(); + BOOST_TEST(msg.find("Invalid metadata schema") == std::string::npos); + BOOST_TEST(msg.find("Schema validation failed") == std::string::npos); + } +} + +BOOST_AUTO_TEST_CASE(update_with_invalid_def_throws) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_update_bad_def"); + + SchemaUpdateRequest upd_req; + SchemaDataReply upd_reply; + upd_req.set_id(id); + upd_req.set_def(INVALID_JSON); + + BOOST_CHECK_THROW( + handler->handleUpdate(TEST_USER, upd_req, upd_reply, log_context), + TraceException); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ============================================================================ +// Test Suite: handleDelete +// ============================================================================ + +BOOST_FIXTURE_TEST_SUITE(HandleDelete, ArangoFixture) + +BOOST_AUTO_TEST_CASE(delete_existing_schema) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_delete"); + + // Remove from cleanup list — we're manually deleting + created_schemas.pop_back(); + + SchemaDeleteRequest del_req; + AckReply del_reply; + del_req.set_id(id); + + BOOST_CHECK_NO_THROW( + handler->handleDelete(TEST_USER, del_req, del_reply, log_context)); + + // Verify it's gone — view should throw + SchemaViewRequest view_req; + SchemaDataReply view_reply; + view_req.set_id(id); + + BOOST_CHECK_THROW( + handler->handleView(TEST_USER, view_req, view_reply, log_context), + TraceException); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ============================================================================ +// Test Suite: handleSearch +// ============================================================================ + +BOOST_FIXTURE_TEST_SUITE(HandleSearch, ArangoFixture) + +BOOST_AUTO_TEST_CASE(search_finds_created_schemas) { + SKIP_IF_UNAVAILABLE(); + + // Create a few schemas with a common prefix + createTestSchema("test_search_a"); + createTestSchema("test_search_b"); + createTestSchema("test_search_c"); + + SchemaSearchRequest search_req; + SchemaDataReply search_reply; + search_req.set_id("test_search_"); + + handler->handleSearch(TEST_USER, search_req, search_reply, log_context); + + BOOST_TEST(search_reply.schema_size() >= 3); +} + +BOOST_AUTO_TEST_CASE(search_returns_def_content) { + SKIP_IF_UNAVAILABLE(); + + createTestSchema("test_search_def"); + + SchemaSearchRequest search_req; + SchemaDataReply search_reply; + search_req.set_id("test_search_def"); + + handler->handleSearch(TEST_USER, search_req, search_reply, log_context); + + BOOST_REQUIRE(search_reply.schema_size() > 0); + + // def should be populated (hydrated from storage) + std::string def = search_reply.schema(0).def(); + BOOST_TEST(!def.empty()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ============================================================================ +// Test Suite: Metadata Validation +// ============================================================================ + +BOOST_FIXTURE_TEST_SUITE(MetadataValidation, ArangoFixture) + +BOOST_AUTO_TEST_CASE(valid_metadata_passes) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_validate_pass"); + + std::string errors = handler->validateMetadataContent( + id, VALID_METADATA, log_context); + + BOOST_TEST(errors.empty()); +} + +BOOST_AUTO_TEST_CASE(missing_required_field_fails) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_validate_missing"); + + std::string errors = handler->validateMetadataContent( + id, INVALID_METADATA_MISSING_REQUIRED, log_context); + + BOOST_TEST(!errors.empty()); +} + +BOOST_AUTO_TEST_CASE(wrong_type_fails) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_validate_type"); + + std::string errors = handler->validateMetadataContent( + id, INVALID_METADATA_WRONG_TYPE, log_context); + + BOOST_TEST(!errors.empty()); +} + +BOOST_AUTO_TEST_CASE(invalid_json_metadata_fails) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_validate_bad_json"); + + std::string errors = handler->validateMetadataContent( + id, INVALID_JSON, log_context); + + BOOST_TEST(!errors.empty()); +} + +BOOST_AUTO_TEST_CASE(nonexistent_schema_returns_error) { + SKIP_IF_UNAVAILABLE(); + + std::string errors = handler->validateMetadataContent( + "does_not_exist:99", VALID_METADATA, log_context); + + // Should return an error string, not throw + BOOST_TEST(!errors.empty()); +} + +BOOST_AUTO_TEST_CASE(handle_metadata_validate_sets_errors_on_failure) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_handle_validate_fail"); + + MetadataValidateRequest req; + MetadataValidateReply reply; + req.set_sch_id(id); + req.set_metadata(INVALID_METADATA_MISSING_REQUIRED); + + handler->handleMetadataValidate(TEST_USER, req, reply, log_context); + + BOOST_TEST(!reply.errors().empty()); +} + +BOOST_AUTO_TEST_CASE(handle_metadata_validate_no_errors_on_success) { + SKIP_IF_UNAVAILABLE(); + + std::string id = createTestSchema("test_handle_validate_pass"); + + MetadataValidateRequest req; + MetadataValidateReply reply; + req.set_sch_id(id); + req.set_metadata(VALID_METADATA); + + handler->handleMetadataValidate(TEST_USER, req, reply, log_context); + + BOOST_TEST(reply.errors().empty()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// ============================================================================ +// Test Suite: Full Lifecycle +// ============================================================================ + +BOOST_FIXTURE_TEST_SUITE(FullLifecycle, ArangoFixture) + +BOOST_AUTO_TEST_CASE(create_validate_update_revise_delete) { + SKIP_IF_UNAVAILABLE(); + + // 1. Create + std::string id = createTestSchema("test_lifecycle", VALID_SCHEMA_DEF, + "lifecycle test v1"); + + // 2. Validate metadata against it + { + std::string errors = handler->validateMetadataContent( + id, VALID_METADATA, log_context); + BOOST_TEST(errors.empty()); + } + + // 3. Validate bad metadata + { + std::string errors = handler->validateMetadataContent( + id, INVALID_METADATA_MISSING_REQUIRED, log_context); + BOOST_TEST(!errors.empty()); + } + + // 4. Update description + { + SchemaUpdateRequest upd_req; + SchemaDataReply upd_reply; + upd_req.set_id(id); + upd_req.set_desc("lifecycle test v1 updated"); + + handler->handleUpdate(TEST_USER, upd_req, upd_reply, log_context); + } + + // 5. View and verify update + { + SchemaViewRequest view_req; + SchemaDataReply view_reply; + view_req.set_id(id); + handler->handleView(TEST_USER, view_req, view_reply, log_context); + + BOOST_REQUIRE(view_reply.schema_size() > 0); + BOOST_TEST(view_reply.schema(0).desc() == "lifecycle test v1 updated"); + } + + // 6. Revise with new definition + std::string id_v2; + { + SchemaReviseRequest rev_req; + SchemaDataReply rev_reply; + rev_req.set_id(id); + rev_req.set_def(UPDATED_SCHEMA_DEF); + rev_req.set_desc("lifecycle test v2"); + + handler->handleRevise(TEST_USER, rev_req, rev_reply, log_context); + + BOOST_REQUIRE(rev_reply.schema_size() > 0); + id_v2 = rev_reply.schema(0).id(); + created_schemas.push_back(id_v2); + + BOOST_TEST(id_v2 != id); + } + + // 7. Validate metadata against new revision + { + std::string errors = handler->validateMetadataContent( + id_v2, VALID_METADATA, log_context); + BOOST_TEST(errors.empty()); + } + + // 8. Delete both versions + { + // Remove from cleanup — we're deleting manually + created_schemas.clear(); + + SchemaDeleteRequest del_req; + AckReply del_reply; + + del_req.set_id(id_v2); + BOOST_CHECK_NO_THROW( + handler->handleDelete(TEST_USER, del_req, del_reply, log_context)); + + del_req.set_id(id); + BOOST_CHECK_NO_THROW( + handler->handleDelete(TEST_USER, del_req, del_reply, log_context)); + } + + // 9. Verify both are gone + { + SchemaViewRequest view_req; + SchemaDataReply view_reply; + + view_req.set_id(id); + BOOST_CHECK_THROW( + handler->handleView(TEST_USER, view_req, view_reply, log_context), + TraceException); + + view_req.set_id(id_v2); + BOOST_CHECK_THROW( + handler->handleView(TEST_USER, view_req, view_reply, log_context), + TraceException); + } +} + +BOOST_AUTO_TEST_SUITE_END() From b1c9b7a91dfae4c3599f269b9682286d8380851c Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Fri, 27 Mar 2026 13:09:07 -0400 Subject: [PATCH 11/15] tests: working integration tests with SchemaHandler. --- core/database/foxx/api/schema_router.js | 2 +- core/server/tests/integration/CMakeLists.txt | 16 ++- .../tests/integration/start_test_arango.sh | 54 ++++++- .../tests/integration/stop_test_arango.sh | 20 +-- .../tests/integration/test_SchemaHandler.cpp | 133 +++++++++++------- 5 files changed, 157 insertions(+), 68 deletions(-) diff --git a/core/database/foxx/api/schema_router.js b/core/database/foxx/api/schema_router.js index 15880f34b..1b0028ed2 100644 --- a/core/database/foxx/api/schema_router.js +++ b/core/database/foxx/api/schema_router.js @@ -209,7 +209,7 @@ router delete sch._key; delete sch._rev; - sch.id = parsed.id + ":" + parsed.ver; + sch.id = parsed.id + ":" + obj.ver; res.send([sch]); }, }); diff --git a/core/server/tests/integration/CMakeLists.txt b/core/server/tests/integration/CMakeLists.txt index 43f68c839..6cb3eadd1 100644 --- a/core/server/tests/integration/CMakeLists.txt +++ b/core/server/tests/integration/CMakeLists.txt @@ -19,6 +19,8 @@ set(ARANGO_ROOT_PASS "test" CACHE STRING "Root password for the ArangoDB test instance") set(ARANGO_PULL "true" CACHE STRING "Pull the ArangoDB image before starting (set to 'false' for offline/local builds)") +set(DATAFED_TEST_DATABASE_NAME "sdms_test" + CACHE STRING "Database name for integration tests (default: sdms_test)") # ── Check for Docker (needed by SchemaAPIClient tests) ─────────────────────── @@ -122,18 +124,18 @@ if(DOCKER_EXECUTABLE) # ======================================================================== set(_arango_env - "ARANGO_IMAGE=${ARANGO_TEST_IMAGE}" - "ARANGO_PORT=${ARANGO_TEST_PORT}" - "ARANGO_ROOT_PASS=${ARANGO_TEST_ROOT_PASS}" + "ARANGO_IMAGE=${ARANGO_IMAGE}" + "ARANGO_PORT=${ARANGO_PORT}" + "ARANGO_ROOT_PASS=${ARANGO_ROOT_PASS}" "ARANGO_CONTAINER=datafed-test-arango" - "ARANGO_PULL=${ARANGO_TEST_PULL}" + "ARANGO_PULL=${ARANGO_PULL}" "DATAFED_PROJECT_ROOT=${DataFed_SOURCE_DIR}" "DATAFED_DATABASE_NAME=${DATAFED_TEST_DATABASE_NAME}" ) - set(_arango_test_env - "ARANGO_TEST_URL=http://localhost:${ARANGO_TEST_PORT}" - "ARANGO_TEST_ROOT_PASS=${ARANGO_TEST_ROOT_PASS}" + "DATAFED_TEST_ARANGO_URL=http://localhost:${ARANGO_PORT}/_db/${DATAFED_TEST_DATABASE_NAME}/api/${DATAFED_FOXX_MAJOR}/" + "DATAFED_TEST_ARANGO_USER=root" + "DATAFED_TEST_ARANGO_PASS=${ARANGO_ROOT_PASS}" "DATAFED_DATABASE_NAME=${ARANGO_TEST_DATABASE_NAME}" ) diff --git a/core/server/tests/integration/start_test_arango.sh b/core/server/tests/integration/start_test_arango.sh index b617addf3..fc72504bc 100755 --- a/core/server/tests/integration/start_test_arango.sh +++ b/core/server/tests/integration/start_test_arango.sh @@ -14,6 +14,7 @@ # ARANGO_ROOT_PASS — Root password (default: "test") # ARANGO_CONTAINER — Container name (default: datafed-test-arango) # ARANGO_PULL — "true" to pull image before starting (default: true) +# DATAFED_DATABASE_NAME — Database name (default: "sdms_test") set -eu PROJECT_ROOT="${DATAFED_PROJECT_ROOT:?DATAFED_PROJECT_ROOT must be set}" @@ -21,6 +22,7 @@ PORT="${ARANGO_PORT:-8529}" ROOT_PASS="${ARANGO_ROOT_PASS:-test}" NAME="${ARANGO_CONTAINER:-datafed-test-arango}" PULL="${ARANGO_PULL:-true}" +DB_NAME="${DATAFED_DATABASE_NAME:-sdms_test}" MAX_WAIT=60 # State file: if we create it, the cleanup step knows to tear down the @@ -90,10 +92,60 @@ fi echo "Provisioning database and Foxx services..." export DATAFED_DATABASE_PASSWORD="${ROOT_PASS}" export DATAFED_DATABASE_HOST="localhost" +export DATAFED_DATABASE_NAME="${DB_NAME}" -bash "${DATAFED_PROJECT_ROOT}/scripts/install_foxx.sh" \ +bash "${PROJECT_ROOT}/scripts/install_foxx.sh" \ -p "${ROOT_PASS}" \ -u "root" \ -i "localhost" echo "Foxx provisioning complete." + +# ── Clean up stale test data ───────────────────────────────────────────────── +# +# Previous test runs may have left behind schemas (e.g. from crashes, or from +# the earlier :null bug). Remove anything with a test_ prefix so the new run +# starts clean. This runs after Foxx provisioning to ensure the database and +# collections exist. + +ARANGO_URL="http://localhost:${PORT}" + +echo "Cleaning stale test schemas from ${DB_NAME}..." +RESULT=$(curl -sf -u "root:${ROOT_PASS}" \ + "${ARANGO_URL}/_db/${DB_NAME}/_api/cursor" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"query": "FOR s IN sch FILTER STARTS_WITH(s.id, \"test_\") REMOVE s IN sch RETURN OLD.id"}' \ + 2>/dev/null) || true + +if [ -n "${RESULT}" ]; then + REMOVED=$(echo "${RESULT}" | grep -o '"result":\[' | wc -l) + if [ "${REMOVED}" -gt 0 ]; then + COUNT=$(echo "${RESULT}" | grep -o '"id"' | wc -l) + echo "Removed ${COUNT} stale test schema(s)" + else + echo "No stale test schemas found" + fi +else + echo "No stale test schemas found (or sch collection does not exist yet)" +fi + +# Also clean up stale schema version edges that reference removed schemas +echo "Cleaning stale schema version edges..." +curl -sf -u "root:${ROOT_PASS}" \ + "${ARANGO_URL}/_db/${DB_NAME}/_api/cursor" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"query": "FOR e IN sch_ver LET fromExists = LENGTH(FOR s IN sch FILTER s._id == e._from LIMIT 1 RETURN 1) LET toExists = LENGTH(FOR s IN sch FILTER s._id == e._to LIMIT 1 RETURN 1) FILTER fromExists == 0 OR toExists == 0 REMOVE e IN sch_ver RETURN OLD._key"}' \ + -o /dev/null 2>/dev/null || true + +# Clean up stale schema dependency edges +echo "Cleaning stale schema dependency edges..." +curl -sf -u "root:${ROOT_PASS}" \ + "${ARANGO_URL}/_db/${DB_NAME}/_api/cursor" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"query": "FOR e IN sch_dep LET fromExists = LENGTH(FOR s IN sch FILTER s._id == e._from LIMIT 1 RETURN 1) LET toExists = LENGTH(FOR s IN sch FILTER s._id == e._to LIMIT 1 RETURN 1) FILTER fromExists == 0 OR toExists == 0 REMOVE e IN sch_dep RETURN OLD._key"}' \ + -o /dev/null 2>/dev/null || true + +echo "Test environment ready." diff --git a/core/server/tests/integration/stop_test_arango.sh b/core/server/tests/integration/stop_test_arango.sh index 0b55ef910..d9be83840 100755 --- a/core/server/tests/integration/stop_test_arango.sh +++ b/core/server/tests/integration/stop_test_arango.sh @@ -12,15 +12,15 @@ set -u NAME="${ARANGO_CONTAINER:-datafed-test-arango}" STATE_FILE="/tmp/${NAME}.started-by-fixture" -if [ -f "${STATE_FILE}" ]; then - if docker rm -f "${NAME}" &>/dev/null; then - echo "Stopped and removed container: ${NAME}" - else - echo "Container ${NAME} was not running (already removed?)" - fi - rm -f "${STATE_FILE}" -else - echo "ArangoDB was pre-existing — leaving it alone" -fi +#if [ -f "${STATE_FILE}" ]; then +# if docker rm -f "${NAME}" &>/dev/null; then +# echo "Stopped and removed container: ${NAME}" +# else +# echo "Container ${NAME} was not running (already removed?)" +# fi +# rm -f "${STATE_FILE}" +#else +# echo "ArangoDB was pre-existing — leaving it alone" +#fi exit 0 diff --git a/core/server/tests/integration/test_SchemaHandler.cpp b/core/server/tests/integration/test_SchemaHandler.cpp index fb888ca08..7a11e8425 100644 --- a/core/server/tests/integration/test_SchemaHandler.cpp +++ b/core/server/tests/integration/test_SchemaHandler.cpp @@ -9,6 +9,8 @@ // Standard includes #include +#include +#include #include #include #include @@ -115,6 +117,19 @@ struct ArangoFixture { try { db = std::make_unique(url, user, pass); + + // Create the test user (ignore errors if it already exists) + try { + UserCreateRequest user_req; + UserDataReply user_reply; + user_req.set_uid("integration_test_user"); + user_req.set_name("Integration Test"); + user_req.set_email("test@test.com"); + db->userCreate(user_req, user_reply, log_context); + } catch (...) { + // User may already exist from a previous run — that's fine + } + handler = std::make_unique(*db); available = true; } catch (std::exception &e) { @@ -172,15 +187,37 @@ struct ArangoFixture { return ""; return val; } -}; -/// Skip macro — avoids nesting every test body in an if block. -#define SKIP_IF_UNAVAILABLE() \ - if (!isAvailable()) { \ - BOOST_TEST_MESSAGE("Skipping: test ArangoDB not configured"); \ - return; \ + /// Poll until ArangoSearch view catches up, or timeout. + SchemaDataReply waitForSearchResults(const std::string &id_prefix, + int min_expected, + int max_attempts = 20, + int interval_ms = 250) { + SchemaSearchRequest search_req; + SchemaDataReply search_reply; + search_req.set_id(id_prefix); + + for (int attempt = 0; attempt < max_attempts; ++attempt) { + search_reply.Clear(); + handler->handleSearch(TEST_USER, search_req, search_reply, log_context); + + if (search_reply.schema_size() >= min_expected) { + return search_reply; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms)); + } + + // Return whatever we got — the caller's BOOST_TEST will report the failure + return search_reply; } +}; + +#define REQUIRE_AVAILABLE() \ + BOOST_REQUIRE_MESSAGE(isAvailable(), \ + "Test ArangoDB not configured — set DATAFED_TEST_ARANGO_URL, " \ + "DATAFED_TEST_ARANGO_USER, and DATAFED_TEST_ARANGO_PASS") // ============================================================================ // Test Suite: handleCreate // ============================================================================ @@ -188,7 +225,7 @@ struct ArangoFixture { BOOST_FIXTURE_TEST_SUITE(HandleCreate, ArangoFixture) BOOST_AUTO_TEST_CASE(create_valid_schema) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_create_valid"); @@ -197,7 +234,7 @@ BOOST_AUTO_TEST_CASE(create_valid_schema) { } BOOST_AUTO_TEST_CASE(create_and_view_round_trip) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_create_view", VALID_SCHEMA_DEF, "round trip test"); @@ -224,7 +261,7 @@ BOOST_AUTO_TEST_CASE(create_and_view_round_trip) { } BOOST_AUTO_TEST_CASE(create_with_invalid_json_throws) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); SchemaCreateRequest req; SchemaDataReply reply; @@ -243,7 +280,7 @@ BOOST_AUTO_TEST_CASE(create_with_invalid_json_throws) { } BOOST_AUTO_TEST_CASE(create_with_missing_properties_throws) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); SchemaCreateRequest req; SchemaDataReply reply; @@ -262,7 +299,7 @@ BOOST_AUTO_TEST_CASE(create_with_missing_properties_throws) { } BOOST_AUTO_TEST_CASE(create_with_missing_type_throws) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); SchemaCreateRequest req; SchemaDataReply reply; @@ -281,7 +318,7 @@ BOOST_AUTO_TEST_CASE(create_with_missing_type_throws) { } BOOST_AUTO_TEST_CASE(create_with_description) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_create_desc", VALID_SCHEMA_DEF, "detailed description here"); @@ -305,7 +342,7 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(HandleRevise, ArangoFixture) BOOST_AUTO_TEST_CASE(revise_creates_new_version) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id_v1 = createTestSchema("test_revise"); @@ -342,7 +379,7 @@ BOOST_AUTO_TEST_CASE(revise_creates_new_version) { } BOOST_AUTO_TEST_CASE(revise_without_def_skips_validation) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_revise_no_def"); @@ -366,7 +403,7 @@ BOOST_AUTO_TEST_CASE(revise_without_def_skips_validation) { } BOOST_AUTO_TEST_CASE(revise_with_invalid_def_throws) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_revise_bad_def"); @@ -389,7 +426,7 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(HandleUpdate, ArangoFixture) BOOST_AUTO_TEST_CASE(update_description_only) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_update_desc", VALID_SCHEMA_DEF, "original description"); @@ -412,7 +449,7 @@ BOOST_AUTO_TEST_CASE(update_description_only) { } BOOST_AUTO_TEST_CASE(update_definition) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_update_def"); @@ -436,7 +473,7 @@ BOOST_AUTO_TEST_CASE(update_definition) { } BOOST_AUTO_TEST_CASE(update_without_def_skips_validation) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_update_no_def"); @@ -457,7 +494,7 @@ BOOST_AUTO_TEST_CASE(update_without_def_skips_validation) { } BOOST_AUTO_TEST_CASE(update_with_invalid_def_throws) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_update_bad_def"); @@ -480,7 +517,7 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(HandleDelete, ArangoFixture) BOOST_AUTO_TEST_CASE(delete_existing_schema) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_delete"); @@ -513,38 +550,36 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(HandleSearch, ArangoFixture) BOOST_AUTO_TEST_CASE(search_finds_created_schemas) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); - // Create a few schemas with a common prefix - createTestSchema("test_search_a"); - createTestSchema("test_search_b"); - createTestSchema("test_search_c"); + createTestSchema("test_search_a"); + createTestSchema("test_search_b"); + createTestSchema("test_search_c"); - SchemaSearchRequest search_req; - SchemaDataReply search_reply; - search_req.set_id("test_search_"); + SchemaDataReply search_reply = waitForSearchResults("test_search_", 3); - handler->handleSearch(TEST_USER, search_req, search_reply, log_context); - - BOOST_TEST(search_reply.schema_size() >= 3); + BOOST_TEST(search_reply.schema_size() >= 3); } BOOST_AUTO_TEST_CASE(search_returns_def_content) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); - createTestSchema("test_search_def"); + std::string id = createTestSchema("test_viewable_schema"); - SchemaSearchRequest search_req; - SchemaDataReply search_reply; - search_req.set_id("test_search_def"); + SchemaDataReply search_reply = waitForSearchResults("test_viewable_schema", 1); - handler->handleSearch(TEST_USER, search_req, search_reply, log_context); + BOOST_REQUIRE(search_reply.schema_size() > 0); - BOOST_REQUIRE(search_reply.schema_size() > 0); + // Search results are lightweight — def is not included. + // Use the ID we already know from create to fetch via view. + SchemaViewRequest view_req; + SchemaDataReply view_reply; + view_req.set_id(id); - // def should be populated (hydrated from storage) - std::string def = search_reply.schema(0).def(); - BOOST_TEST(!def.empty()); + handler->handleView(TEST_USER, view_req, view_reply, log_context); + + BOOST_REQUIRE(view_reply.schema_size() > 0); + BOOST_TEST(!view_reply.schema(0).def().empty()); } BOOST_AUTO_TEST_SUITE_END() @@ -556,7 +591,7 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(MetadataValidation, ArangoFixture) BOOST_AUTO_TEST_CASE(valid_metadata_passes) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_validate_pass"); @@ -567,7 +602,7 @@ BOOST_AUTO_TEST_CASE(valid_metadata_passes) { } BOOST_AUTO_TEST_CASE(missing_required_field_fails) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_validate_missing"); @@ -578,7 +613,7 @@ BOOST_AUTO_TEST_CASE(missing_required_field_fails) { } BOOST_AUTO_TEST_CASE(wrong_type_fails) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_validate_type"); @@ -589,7 +624,7 @@ BOOST_AUTO_TEST_CASE(wrong_type_fails) { } BOOST_AUTO_TEST_CASE(invalid_json_metadata_fails) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_validate_bad_json"); @@ -600,7 +635,7 @@ BOOST_AUTO_TEST_CASE(invalid_json_metadata_fails) { } BOOST_AUTO_TEST_CASE(nonexistent_schema_returns_error) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string errors = handler->validateMetadataContent( "does_not_exist:99", VALID_METADATA, log_context); @@ -610,7 +645,7 @@ BOOST_AUTO_TEST_CASE(nonexistent_schema_returns_error) { } BOOST_AUTO_TEST_CASE(handle_metadata_validate_sets_errors_on_failure) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_handle_validate_fail"); @@ -625,7 +660,7 @@ BOOST_AUTO_TEST_CASE(handle_metadata_validate_sets_errors_on_failure) { } BOOST_AUTO_TEST_CASE(handle_metadata_validate_no_errors_on_success) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); std::string id = createTestSchema("test_handle_validate_pass"); @@ -648,7 +683,7 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE(FullLifecycle, ArangoFixture) BOOST_AUTO_TEST_CASE(create_validate_update_revise_delete) { - SKIP_IF_UNAVAILABLE(); + REQUIRE_AVAILABLE(); // 1. Create std::string id = createTestSchema("test_lifecycle", VALID_SCHEMA_DEF, From 74ed80978e5a2ae9f4af23026afe5ee64973c904 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Fri, 27 Mar 2026 17:09:40 +0000 Subject: [PATCH 12/15] chore: Auto-format shell scripts with shfmt --- core/server/tests/integration/start_test_arango.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/server/tests/integration/start_test_arango.sh b/core/server/tests/integration/start_test_arango.sh index fc72504bc..8fd5a66aa 100755 --- a/core/server/tests/integration/start_test_arango.sh +++ b/core/server/tests/integration/start_test_arango.sh @@ -34,8 +34,8 @@ rm -f "${STATE_FILE}" # ── Check for an already-running instance ──────────────────────────────────── if curl -sf -o /dev/null \ - -u "root:${ROOT_PASS}" \ - "http://localhost:${PORT}/_api/version" 2>/dev/null; then + -u "root:${ROOT_PASS}" \ + "http://localhost:${PORT}/_api/version" 2>/dev/null; then echo "ArangoDB already reachable on port ${PORT} — reusing existing instance" else # ── No instance found — start a container ──────────────────────────────── From 7d93b1dd712323d494d74fb43bdca5117b17467a Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Fri, 27 Mar 2026 13:21:52 -0400 Subject: [PATCH 13/15] update: leave integration tests off in core build for now. --- core/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/docker/Dockerfile b/core/docker/Dockerfile index e0714d2c7..4a9bce50f 100644 --- a/core/docker/Dockerfile +++ b/core/docker/Dockerfile @@ -65,7 +65,7 @@ RUN ${DATAFED_DEPENDENCIES_ROOT}/scripts/generate_dependencies_config.sh && \ -DBUILD_PYTHON_CLIENT=False \ -DBUILD_FOXX=False \ -DENABLE_UNIT_TESTS=True \ - -DENABLE_INTEGRATION_TESTS=True + -DENABLE_INTEGRATION_TESTS=False RUN ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake --build build -j 8 RUN ${DATAFED_DEPENDENCIES_INSTALL_PATH}/bin/cmake --build build --target install From 9327dd463a6be94758cb4193ea2c2bb9bc663b27 Mon Sep 17 00:00:00 2001 From: Joshua S Brown Date: Fri, 27 Mar 2026 13:55:57 -0400 Subject: [PATCH 14/15] fix: uncomment commented out code block. --- .../tests/integration/stop_test_arango.sh | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/server/tests/integration/stop_test_arango.sh b/core/server/tests/integration/stop_test_arango.sh index d9be83840..0b55ef910 100755 --- a/core/server/tests/integration/stop_test_arango.sh +++ b/core/server/tests/integration/stop_test_arango.sh @@ -12,15 +12,15 @@ set -u NAME="${ARANGO_CONTAINER:-datafed-test-arango}" STATE_FILE="/tmp/${NAME}.started-by-fixture" -#if [ -f "${STATE_FILE}" ]; then -# if docker rm -f "${NAME}" &>/dev/null; then -# echo "Stopped and removed container: ${NAME}" -# else -# echo "Container ${NAME} was not running (already removed?)" -# fi -# rm -f "${STATE_FILE}" -#else -# echo "ArangoDB was pre-existing — leaving it alone" -#fi +if [ -f "${STATE_FILE}" ]; then + if docker rm -f "${NAME}" &>/dev/null; then + echo "Stopped and removed container: ${NAME}" + else + echo "Container ${NAME} was not running (already removed?)" + fi + rm -f "${STATE_FILE}" +else + echo "ArangoDB was pre-existing — leaving it alone" +fi exit 0 From 81c774cb9ec3e35ae674a1a3bd2a3ec1e5abfd7d Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Fri, 27 Mar 2026 15:32:11 -0400 Subject: [PATCH 15/15] style: fix javascript formatting. --- web/static/dlg_schema.js | 13 +++++++++++-- web/static/dlg_schema_list.js | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/web/static/dlg_schema.js b/web/static/dlg_schema.js index ee96b6ad1..da40f6807 100644 --- a/web/static/dlg_schema.js +++ b/web/static/dlg_schema.js @@ -11,13 +11,22 @@ export const mode_rev = 3; const dlg_title = ["View", "Edit", "Create New", "Create Revision of "]; const btn_title = ["Close", "Save", "Create", "Create"]; -/** Strip version suffix from a composite schema ID (e.g. "foo:0" -> "foo") */ +/** + * Strip version suffix from a composite schema ID (e.g. "foo:0" -> "foo"). + * @param {string} id - Composite schema ID. + * @returns {string} Schema name without version suffix. + */ function schemaName(id) { var idx = id.indexOf(":"); return idx !== -1 ? id.substring(0, idx) : id; } -/** Ensure a schema ID has the :version suffix */ +/** + * Ensure a schema ID has the :version suffix. + * @param {string} id - Schema ID, possibly without version. + * @param {string|number} ver - Version to append if missing. + * @returns {string} Schema ID with version suffix. + */ function schemaId(id, ver) { return id.indexOf(":") !== -1 ? id : id + ":" + ver; } diff --git a/web/static/dlg_schema_list.js b/web/static/dlg_schema_list.js index e3fa5c11f..a5e7fa123 100644 --- a/web/static/dlg_schema_list.js +++ b/web/static/dlg_schema_list.js @@ -8,13 +8,22 @@ import * as dlgSchema from "./dlg_schema.js"; var tree, dlg_inst, frame; -/** Strip version suffix from a composite schema ID (e.g. "foo:0" -> "foo") */ +/** + * Strip version suffix from a composite schema ID (e.g. "foo:0" -> "foo"). + * @param {string} id - Composite schema ID. + * @returns {string} Schema name without version suffix. + */ function schemaName(id) { var idx = id.indexOf(":"); return idx !== -1 ? id.substring(0, idx) : id; } -/** Ensure a schema ID has the :version suffix */ +/** + * Ensure a schema ID has the :version suffix. + * @param {string} id - Schema ID, possibly without version. + * @param {string|number} ver - Version to append if missing. + * @returns {string} Schema ID with version suffix. + */ function schemaId(id, ver) { return id.indexOf(":") !== -1 ? id : id + ":" + ver; }