diff --git a/.ahoy.yml b/.ahoy.yml index cb6bad7..4b1a5e6 100644 --- a/.ahoy.yml +++ b/.ahoy.yml @@ -19,6 +19,7 @@ commands: build-network: usage: Ensure that the amazeeio network exists. cmd: | + ahoy title "Creating amazeeio Docker network" docker network prune -f > /dev/null docker network inspect amazeeio-network > /dev/null || docker network create amazeeio-network @@ -27,40 +28,47 @@ commands: cmd: | ahoy line "Project : " ${PROJECT} ahoy line "Site local URL : " ${LAGOON_LOCALDEV_URL} - ahoy line "DB port on host : " $(docker port $(docker-compose ps -q postgres) 5432 | cut -d : -f 2) - ahoy line "Solr port on host : " $(docker port $(docker-compose ps -q solr) 8983 | cut -d : -f 2) + ahoy line "DB port on host : " $(docker port $(sh bin/docker-compose.sh ps -q postgres) 5432 | cut -d : -f 2) + ahoy line "Solr port on host : " $(docker port $(sh bin/docker-compose.sh ps -q solr) 8983 | cut -d : -f 2) ahoy line "Mailhog URL : " http://mailhog.docker.amazee.io/ up: usage: Build and start Docker containers. cmd: | - docker-compose up -d "$@" - ahoy cli '$APP_DIR/bin/init.sh' + ahoy title "Building and starting Docker containers" + sh bin/docker-compose.sh up -d "$@" + echo "Initialising database schema" + ahoy cli '"${APP_DIR}"/bin/init.sh' + echo "Waiting for containers to start listening..." ahoy cli "dockerize -wait tcp://ckan:5000 -timeout 1m" - if docker-compose logs | grep -q "\[Error\]"; then exit 1; fi - if docker-compose logs | grep -q "Exception"; then exit 1; fi + if sh bin/docker-compose.sh logs | grep -q "\[Error\]"; then exit 1; fi + if sh bin/docker-compose.sh logs | grep -q "Exception"; then exit 1; fi docker ps -a --filter name=^/${COMPOSE_PROJECT_NAME}_ export DOCTOR_CHECK_CLI=0 down: usage: Stop Docker containers and remove container, images, volumes and networks. - cmd: 'if [ -f "docker-compose.yml" ]; then docker-compose down --volumes; fi' + cmd: | + ahoy title 'Stopping and removing old containers, images, volumes, networks' + if [ -f "docker-compose.yml" ]; then sh bin/docker-compose.sh down --volumes; fi start: usage: Start existing Docker containers. - cmd: docker-compose start "$@" + cmd: sh bin/docker-compose.sh start "$@" stop: usage: Stop running Docker containers. - cmd: docker-compose stop "$@" + cmd: sh bin/docker-compose.sh stop "$@" restart: usage: Restart all stopped and running Docker containers. - cmd: docker-compose restart "$@" + cmd: sh bin/docker-compose.sh restart "$@" logs: usage: Show Docker logs. - cmd: docker-compose logs "$@" + cmd: | + ahoy title "Output logs" + sh bin/docker-compose.sh logs "$@" pull: usage: Pull latest docker images. @@ -68,7 +76,13 @@ commands: cli: usage: Start a shell inside CLI container or run a command. - cmd: if \[ "${#}" -ne 0 \]; then docker exec $(docker-compose ps -q ckan) sh -c '. ${APP_DIR}/bin/activate; cd $APP_DIR;'" $*"; else docker exec $(docker-compose ps -q ckan) sh -c '. ${APP_DIR}/bin/activate && cd $APP_DIR && sh'; fi + cmd: | + CKAN_CONTAINER=$(sh bin/docker-compose.sh ps -q ckan) + if [ "${#}" -ne 0 \]; then + docker exec $CKAN_CONTAINER sh -c '. "${APP_DIR}"/bin/activate; cd $APP_DIR;'" $*" + else + docker exec $CKAN_CONTAINER sh -c '. "${APP_DIR}"/bin/activate && cd $APP_DIR && sh' + fi doctor: usage: Find problems with current project setup. @@ -78,11 +92,12 @@ commands: usage: Install test site data. cmd: | ahoy title "Installing a fresh site" - ahoy cli '$APP_DIR/bin/init.sh && $APP_DIR/bin/create-test-data.sh' + ahoy cli '"${APP_DIR}"/bin/init.sh && "${APP_DIR}"/bin/create-test-data.sh' clean: usage: Remove containers and all build files. cmd: | + ahoy title "Cleaning up old builds" ahoy down # Remove other directories. # @todo: Add destinations below. @@ -99,20 +114,21 @@ commands: flush-redis: usage: Flush Redis cache. - cmd: docker exec -i $(docker-compose ps -q redis) redis-cli flushall > /dev/null + cmd: docker exec -i $(sh bin/docker-compose.sh ps -q redis) redis-cli flushall > /dev/null lint: usage: Lint code. cmd: | + ahoy title 'Check for lint' ahoy cli "flake8 ${@:-ckanext}" || \ [ "${ALLOW_LINT_FAIL:-0}" -eq 1 ] copy-local-files: usage: Update files from local repo. cmd: | - docker cp . $(docker-compose ps -q ckan):/srv/app/ - docker cp bin/ckan_cli $(docker-compose ps -q ckan):/usr/bin/ - ahoy cli 'chmod -v u+x /usr/bin/ckan_cli $APP_DIR/bin/*; cp -v .docker/test.ini $CKAN_INI; $APP_DIR/bin/process-config.sh' + docker cp . $(sh bin/docker-compose.sh ps -q ckan):/srv/app/ + docker cp bin/ckan_cli $(sh bin/docker-compose.sh ps -q ckan):/usr/bin/ + ahoy cli 'chmod -v u+x /usr/bin/ckan_cli "${APP_DIR}"/bin/*; cp -v .docker/test.ini $CKAN_INI; "${APP_DIR}"/bin/process-config.sh' pip-list: usage: List pip install version details @@ -122,27 +138,29 @@ commands: test-unit: usage: Run unit tests. cmd: | - ahoy cli 'pytest --ckan-ini=${CKAN_INI} --cov=ckanext $APP_DIR/ckanext' || \ + ahoy title 'Run unit tests' + ahoy cli 'pytest --ckan-ini=${CKAN_INI} --cov=ckanext "${APP_DIR}"/ckanext --junit-xml=test/junit/results.xml' || \ [ "${ALLOW_UNIT_FAIL:-0}" -eq 1 ] test-bdd: usage: Run BDD tests. cmd: | + ahoy title 'Run scenario tests' ahoy cli "rm -f test/screenshots/*" ahoy start-mailmock & - sleep 5 && + sleep 5 if [ "$BEHAVE_TAG" = "" ]; then # no tag specified, probably running locally - (ahoy cli "behave -k ${*:-test/features} --tags=smoke" \ - && ahoy cli "behave -k ${*:-test/features} --tags=-smoke" \ + (ahoy cli "behave --junit -k ${*:-test/features} --tags=smoke --junit-directory=test/junit/" \ + && ahoy cli "behave --junit -k ${*:-test/features} --tags=-smoke --junit-directory=test/junit/" ) || [ "${ALLOW_BDD_FAIL:-0}" -eq 1 ] elif [ "$BEHAVE_TAG" = "authenticated" ]; then # run any tests that don't have a specific tag - ahoy cli "behave -k ${*:-test/features} --tags=-unauthenticated --tags=-smoke" \ + ahoy cli "behave --junit -k ${*:-test/features} --tags=-unauthenticated --tags=-smoke --junit-directory=test/junit/" \ || [ "${ALLOW_BDD_FAIL:-0}" -eq 1 ] else # run tests with the specified tag - ahoy cli "behave -k ${*:-test/features} --tags=$BEHAVE_TAG" \ + ahoy cli "behave --junit -k ${*:-test/features} --tags=$BEHAVE_TAG --junit-directory=test/junit/" \ || [ "${ALLOW_BDD_FAIL:-0}" -eq 1 ] fi ahoy stop-mailmock @@ -151,7 +169,7 @@ commands: usage: Starts email mock server used for email BDD tests cmd: | ahoy title 'Starting mailmock' - ahoy cli 'mailmock -p 8025 -o ${APP_DIR}/test/emails' # for debugging mailmock email output remove --no-stdout + ahoy cli 'mailmock -p 8025 -o "${APP_DIR}"/test/emails' # for debugging mailmock email output remove --no-stdout stop-mailmock: usage: Stops email mock server used for email BDD tests diff --git a/.docker/Dockerfile-template.ckan b/.docker/Dockerfile-template.ckan index 53acf91..73247aa 100644 --- a/.docker/Dockerfile-template.ckan +++ b/.docker/Dockerfile-template.ckan @@ -1,4 +1,8 @@ -FROM openknowledge/ckan-dev:{CKAN_VERSION} +FROM ckan/ckan-dev:{CKAN_VERSION} + +# swap between root and unprivileged user +ARG ORIGINAL_USER +RUN ORIGINAL_USER=$(id -un) ARG SITE_URL=http://ckan:5000/ ENV PYTHON_VERSION={PYTHON_VERSION} @@ -8,11 +12,22 @@ ENV PYTHON={PYTHON} WORKDIR "${APP_DIR}" -ENV DOCKERIZE_VERSION v0.6.1 -RUN apk add --no-cache build-base \ - && curl -sL https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz \ +COPY .docker/test.ini $CKAN_INI + +COPY . "${APP_DIR}"/ + +USER root + +COPY bin/ckan_cli /usr/bin/ + +RUN chmod +x "${APP_DIR}"/bin/*.sh /usr/bin/ckan_cli + +ENV DOCKERIZE_VERSION=v0.6.1 +RUN wget -O - https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-linux-amd64-${DOCKERIZE_VERSION}.tar.gz \ | tar -C /usr/local/bin -xzvf - +RUN which ps || apt-get install -y procps + # Install CKAN. RUN cd $SRC_DIR/ckan \ @@ -22,15 +37,9 @@ RUN cd $SRC_DIR/ckan \ && git reset --hard && git clean -f \ && git checkout '{CKAN_GIT_VERSION}' -COPY .docker/test.ini $CKAN_INI - -COPY . ${APP_DIR}/ - -COPY bin/ckan_cli /usr/bin/ - -RUN chmod +x ${APP_DIR}/bin/*.sh /usr/bin/ckan_cli +USER "$ORIGINAL_USER" # Init current extension. -RUN ${APP_DIR}/bin/init-ext.sh +RUN "${APP_DIR}"/bin/init-ext.sh CMD ["/srv/app/bin/serve.sh"] diff --git a/.docker/test.ini b/.docker/test.ini index ee04a5f..78aba96 100644 --- a/.docker/test.ini +++ b/.docker/test.ini @@ -1,17 +1,3 @@ -#UNIT TEST CONFIG -# -# CKAN - Pylons configuration -# -# These are some of the configuration options available for your CKAN -# instance. Check the documentation in 'doc/configuration.rst' or at the -# following URL for a description of what they do and the full list of -# available options: -# -# http://docs.ckan.org/en/latest/maintaining/configuration.html -# -# The %(here)s variable will be replaced with the parent directory of this file -# - [DEFAULT] debug = false smtp_server = localhost:8025 @@ -34,6 +20,7 @@ beaker.session.key = ckan # This is the secret token that the beaker library uses to hash the cookie sent # to the client. `paster make-config` generates a unique value for this each # time it generates a config file. +SECRET_KEY = bSmgPpaxg2M+ZRes3u1TXwIcE beaker.session.secret = bSmgPpaxg2M+ZRes3u1TXwIcE # `paster make-config` generates a unique value for this each time it generates @@ -79,15 +66,12 @@ ckan.auth.roles_that_cascade_to_sub_groups = admin ckan.auth.public_user_details = False ## Plugins Settings + ckan.plugins = scheming_datasets resource_visibility -# Define which views should be created by default -# (plugins must be loaded in ckan.plugins) -ckan.views.default_views = image_view text_view recline_view - # Customize which text formats the text_view plugin will show #ckan.preview.json_formats = json #ckan.preview.xml_formats = xml rdf rdf+xml owl+xml atom rss @@ -97,7 +81,7 @@ ckan.views.default_views = image_view text_view recline_view #ckan.preview.image_formats = png jpeg jpg gif ## Internationalisation Settings -ckan.locale_default = en_AU +ckan.locale_default = en ckan.locale_order = en pt_BR ja it cs_CZ ca es fr el sv sr sr@latin no sk fi ru de pl nl bg ko_KR hu sa sl lv ckan.locales_offered = ckan.locales_filtered_out = en_AU diff --git a/.env b/.env index b435428..c2a56b7 100644 --- a/.env +++ b/.env @@ -13,7 +13,7 @@ PROJECT="ckanext-resource-visibility" # Docker Compose project name. All containers will have this name. -COMPOSE_PROJECT_NAME="$PROJECT" +COMPOSE_PROJECT_NAME="ckanext-resource-visibility" # Flag to allow code linting failures. 0=enforce, 1=ignore ALLOW_LINT_FAIL=0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bee6580..2f2b652 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,12 +7,13 @@ on: - master jobs: + # Quick check so we don't waste minutes if there's a Flake8 error lint: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install requirements @@ -25,43 +26,65 @@ jobs: strategy: fail-fast: false matrix: - ckan-version: ["2.10", 2.9, 2.9-py2] + ckan-version: ["2.11", "2.10", 2.9] + experimental: [false] + include: + - ckan-version: 'master' + experimental: true #master is unstable, good to know if we are compatible or not - name: CKAN ${{ matrix.ckan-version }} + name: Test on CKAN ${{ matrix.ckan-version }} runs-on: ubuntu-latest - container: drevops/ci-builder + container: drevops/ci-runner:23.12.0 env: CKAN_VERSION: ${{ matrix.ckan-version }} steps: - - uses: actions/checkout@v3 + # Patch https://github.com/actions/runner/issues/863 + - name: Preserve $HOME set in the container + run: echo HOME=/root >> "$GITHUB_ENV" + + - uses: actions/checkout@v4 + continue-on-error: ${{ matrix.experimental }} timeout-minutes: 2 - name: Build + continue-on-error: ${{ matrix.experimental }} run: bin/build.sh timeout-minutes: 15 - name: Unit test + continue-on-error: ${{ matrix.experimental }} run: bin/test.sh timeout-minutes: 10 - name: Scenario test + continue-on-error: ${{ matrix.experimental }} run: bin/test-bdd.sh timeout-minutes: 30 - name: Retrieve logs - if: failure() + if: always() run: ahoy logs - timeout-minutes: 5 + continue-on-error: ${{ matrix.experimental }} + timeout-minutes: 1 - - name: Retrieve screenshots - if: failure() + - name: Retrieve results + if: always() run: bin/process-artifacts.sh + continue-on-error: ${{ matrix.experimental }} timeout-minutes: 1 + - name: Test Summary + uses: test-summary/action@v2 + continue-on-error: ${{ matrix.experimental }} + with: + paths: "/tmp/artifacts/junit/*.xml" + if: always() + - name: Upload screenshots - if: failure() - uses: actions/upload-artifact@v3 + if: always() + uses: actions/upload-artifact@v4 + continue-on-error: ${{ matrix.experimental }} with: name: CKAN ${{ matrix.ckan-version }} screenshots path: /tmp/artifacts/behave/screenshots diff --git a/bin/build.sh b/bin/build.sh index 4854247..a2382f0 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -14,32 +14,25 @@ sed -i -e "s/##//" docker-compose.yml # Pull the latest images. ahoy pull -PYTHON=python +PYTHON_VERSION=py3 +PYTHON="python3" CKAN_GIT_VERSION=$CKAN_VERSION CKAN_GIT_ORG=ckan -if [ "$CKAN_VERSION" = "2.10" ]; then +if [ "$CKAN_VERSION" = "2.11" ]; then + CKAN_GIT_VERSION=ckan-2.11.0 +elif [ "$CKAN_VERSION" = "2.10" ]; then if [ "$CKAN_TYPE" = "custom" ]; then CKAN_GIT_VERSION=ckan-2.10.0-qgov.1 CKAN_GIT_ORG=qld-gov-au fi - PYTHON_VERSION=py3 - PYTHON="${PYTHON}3" else if [ "$CKAN_TYPE" = "custom" ]; then CKAN_GIT_VERSION=ckan-2.9.5-qgov.9 CKAN_GIT_ORG=qld-gov-au fi - - if [ "$CKAN_VERSION" = "2.9-py2" ]; then - PYTHON_VERSION=py2 - CKAN_GIT_VERSION=2.9 - else - PYTHON_VERSION=py3 - PYTHON="${PYTHON}3" - fi fi sed "s|{CKAN_VERSION}|$CKAN_VERSION|g" .docker/Dockerfile-template.ckan \ diff --git a/bin/create-test-data.sh b/bin/create-test-data.sh index ff88b69..2ebd0c5 100644 --- a/bin/create-test-data.sh +++ b/bin/create-test-data.sh @@ -2,15 +2,14 @@ ## # Create some example content for extension BDD tests. # -set -e -set -x +set -ex CKAN_ACTION_URL=${CKAN_SITE_URL}api/action CKAN_USER_NAME="${CKAN_USER_NAME:-admin}" CKAN_DISPLAY_NAME="${CKAN_DISPLAY_NAME:-Administrator}" CKAN_USER_EMAIL="${CKAN_USER_EMAIL:-admin@localhost}" -. ${APP_DIR}/bin/activate +. "${APP_DIR}"/bin/activate add_user_if_needed () { echo "Adding user '$2' ($1) with email address [$3]" @@ -44,30 +43,27 @@ add_user_if_needed test_org_member "Test Member" test_org_member@localhost echo "Creating ${TEST_ORG_TITLE} organisation:" +api_call () { + wget -O - --header="Authorization: ${API_KEY}" --post-data "$1" ${CKAN_ACTION_URL}/$2 +} + TEST_ORG=$( \ - curl -LsH "Authorization: ${API_KEY}" \ - --data '{"name": "'"${TEST_ORG_NAME}"'", "title": "'"${TEST_ORG_TITLE}"'", - "description": "Organisation for testing issues"}' \ - ${CKAN_ACTION_URL}/organization_create + api_call '{"name": "'"${TEST_ORG_NAME}"'", "title": "'"${TEST_ORG_TITLE}"'", + "description": "Organisation for testing issues"}' organization_create ) -TEST_ORG_ID=$(echo $TEST_ORG | $PYTHON ${APP_DIR}/bin/extract-id.py) +TEST_ORG_ID=$(echo $TEST_ORG | $PYTHON "${APP_DIR}"/bin/extract-id.py) echo "Assigning test users to '${TEST_ORG_TITLE}' organisation (${TEST_ORG_ID}):" -curl -LsH "Authorization: ${API_KEY}" \ - --data '{"id": "'"${TEST_ORG_ID}"'", "object": "test_org_admin", "object_type": "user", "capacity": "admin"}' \ - ${CKAN_ACTION_URL}/member_create +api_call '{"id": "'"${TEST_ORG_ID}"'", "object": "test_org_admin", "object_type": "user", "capacity": "admin"}' member_create + +api_call '{"id": "'"${TEST_ORG_ID}"'", "object": "test_org_editor", "object_type": "user", "capacity": "editor"}' member_create -curl -LsH "Authorization: ${API_KEY}" \ - --data '{"id": "'"${TEST_ORG_ID}"'", "object": "test_org_editor", "object_type": "user", "capacity": "editor"}' \ - ${CKAN_ACTION_URL}/member_create +api_call '{"id": "'"${TEST_ORG_ID}"'", "object": "test_org_member", "object_type": "user", "capacity": "member"}' member_create -curl -LsH "Authorization: ${API_KEY}" \ - --data '{"id": "'"${TEST_ORG_ID}"'", "object": "test_org_member", "object_type": "user", "capacity": "member"}' \ - ${CKAN_ACTION_URL}/member_create ## # END. # -. ${APP_DIR}/bin/deactivate +. "${APP_DIR}"/bin/deactivate diff --git a/bin/docker-compose.sh b/bin/docker-compose.sh new file mode 100644 index 0000000..82f4a05 --- /dev/null +++ b/bin/docker-compose.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -x + +# Pass commands to Docker Compose v1 or v2 depending on what is present + +if (docker compose ls >/dev/null); then + # Docker Compose v2 + docker compose $* +elif (which docker-compose >/dev/null); then + # Docker Compose v1 + docker-compose $* +else + # Docker Compose not found + exit 1 +fi diff --git a/bin/doctor.sh b/bin/doctor.sh index 9c7f455..2a3ca3f 100755 --- a/bin/doctor.sh +++ b/bin/doctor.sh @@ -31,7 +31,6 @@ main() { if [ "${DOCTOR_CHECK_TOOLS}" == "1" ]; then [ "$(command_exists docker)" == "1" ] && error "Please install Docker (https://www.docker.com/get-started)" && exit 1 - [ "$(command_exists docker-compose)" == "1" ] && error "Please install docker-compose (https://docs.docker.com/compose/install/)" && exit 1 [ "$(command_exists composer)" == "1" ] && error "Please install Composer (https://getcomposer.org/)" && exit 1 [ "$(command_exists pygmy)" == "1" ] && error "Please install Pygmy (https://pygmy.readthedocs.io/)" && exit 1 [ "$(command_exists ahoy)" == "1" ] && error "Please install Ahoy (https://ahoy-cli.readthedocs.io/)" && exit 1 @@ -55,7 +54,7 @@ main() { # Check that the stack is running. if [ "${DOCTOR_CHECK_CLI}" == "1" ]; then - if ! docker ps -q --no-trunc | grep "$(docker-compose ps -q ckan)" > /dev/null 2>&1; then + if ! docker ps -q --no-trunc | grep "$(sh bin/docker-compose.sh ps -q ckan)" > /dev/null 2>&1; then error "CLI container is not running. Run 'ahoy up'." exit 1 fi @@ -94,7 +93,7 @@ main() { fi # Check that the volume is mounted into CLI container. - if ! docker exec -i "$(docker-compose ps -q ckan)" sh -c "grep \"^/dev\" /etc/mtab|grep -q /tmp/amazeeio_ssh-agent"; then + if ! docker exec -i "$(sh bin/docker-compose.sh ps -q ckan)" sh -c "grep \"^/dev\" /etc/mtab|grep -q /tmp/amazeeio_ssh-agent"; then error "SSH key is added to Pygmy, but the volume is not mounted into container. Make sure that your your \"docker-compose.yml\" has the following lines:" error "volumes_from:" error " - container:amazeeio-ssh-agent" @@ -103,7 +102,7 @@ main() { fi # Check that ssh key is available in the container. - if ! docker exec -i "$(docker-compose ps -q ckan)" bash -c "ssh-add -L | grep -q 'ssh-rsa'" ; then + if ! docker exec -i "$(sh bin/docker-compose.sh ps -q ckan)" bash -c "ssh-add -L | grep -q 'ssh-rsa'" ; then error "SSH key was not added into container. Run 'ahoy up -- --build'." exit 1 fi @@ -113,7 +112,7 @@ main() { if [ "${DOCTOR_CHECK_WEBSERVER}" == "1" ]; then - host_app_port="$(docker port $(docker-compose ps -q ckan) $APP_PORT | cut -d : -f 2)" + host_app_port="$(docker port $(sh bin/docker-compose.sh ps -q ckan) $APP_PORT | cut -d : -f 2)" if ! curl -L -s -o /dev/null -w "%{http_code}" "${LAGOON_LOCALDEV_URL}:${host_app_port}" | grep -q 200; then error "Web server is not accessible at ${LAGOON_LOCALDEV_URL}:${host_app_port}" exit 1 @@ -122,7 +121,7 @@ main() { fi if [ "${DOCTOR_CHECK_BOOTSTRAP}" == "1" ]; then - host_app_port="$(docker port $(docker-compose ps -q ckan) $APP_PORT | cut -d : -f 2)" + host_app_port="$(docker port $(sh bin/docker-compose.sh ps -q ckan) $APP_PORT | cut -d : -f 2)" if ! curl -L -s -N "${LAGOON_LOCALDEV_URL}:${host_app_port}" | grep -q -i "meta name=\"generator\" content=\"ckan"; then error "Website is running, but cannot be bootstrapped. Try pulling latest container images with 'ahoy pull'" exit 1 diff --git a/bin/init-ext.sh b/bin/init-ext.sh index de4e638..350aba5 100755 --- a/bin/init-ext.sh +++ b/bin/init-ext.sh @@ -2,8 +2,7 @@ ## # Install current extension. # -set -e -set -x +set -ex install_requirements () { PROJECT_DIR=$1 @@ -33,8 +32,10 @@ install_requirements () { done } -. ${APP_DIR}/bin/activate - +. "${APP_DIR}"/bin/activate +if [ "$CKAN_VERSION" = "2.9" ]; then + pip install "setuptools>=44.1.0,<71" +fi install_requirements . dev-requirements requirements-dev for extension in . `ls -d $SRC_DIR/ckanext-*`; do install_requirements $extension requirements pip-requirements @@ -45,5 +46,5 @@ installed_name=$(grep '^\s*name=' setup.py |sed "s|[^']*'\([-a-zA-Z0-9]*\)'.*|\1 # Validate that the extension was installed correctly. if ! pip list | grep "$installed_name" > /dev/null; then echo "Unable to find the extension in the list"; exit 1; fi -. $APP_DIR/bin/process-config.sh -. ${APP_DIR}/bin/deactivate +. "${APP_DIR}"/bin/process-config.sh +. "${APP_DIR}"/bin/deactivate diff --git a/bin/init.sh b/bin/init.sh index 6423709..e000532 100755 --- a/bin/init.sh +++ b/bin/init.sh @@ -4,7 +4,7 @@ # set -e -. ${APP_DIR}/bin/activate +. "${APP_DIR}"/bin/activate CLICK_ARGS="--yes" ckan_cli db clean ckan_cli db init ckan_cli db upgrade diff --git a/bin/process-artifacts.sh b/bin/process-artifacts.sh index 6324f60..b2066b5 100755 --- a/bin/process-artifacts.sh +++ b/bin/process-artifacts.sh @@ -6,8 +6,9 @@ set -e # Create screenshots directory in case it was not created before. This is to # avoid this script to fail when copying artifacts. -ahoy cli "mkdir -p test/screenshots" +ahoy cli "mkdir -p test/screenshots test/junit" # Copy from the app container to the build host for storage. -mkdir -p /tmp/artifacts/behave -docker cp "$(docker-compose ps -q ckan)":/srv/app/test/screenshots /tmp/artifacts/behave/ +mkdir -p /tmp/artifacts/behave /tmp/artifacts/junit +docker cp "$(sh bin/docker-compose.sh ps -q ckan)":/srv/app/test/screenshots /tmp/artifacts/behave/ +docker cp "$(sh bin/docker-compose.sh ps -q ckan)":/srv/app/test/junit /tmp/artifacts/ diff --git a/bin/serve.sh b/bin/serve.sh index ed1a20a..a4a5a4b 100755 --- a/bin/serve.sh +++ b/bin/serve.sh @@ -1,23 +1,9 @@ #!/usr/bin/env sh set -e -dockerize -wait tcp://postgres:5432 -timeout 1m -dockerize -wait tcp://solr:8983 -timeout 1m -dockerize -wait tcp://redis:6379 -timeout 1m - -for i in `seq 1 60`; do - if (PGPASSWORD=pass psql -h postgres -U ckan_default -d ckan_test -c "\q"); then - echo "Database became ready on attempt $i" - break - else - echo "Database not yet ready, retrying (attempt $i)..." - sleep 1 - fi -done - -. ${APP_DIR}/bin/activate +. "${APP_DIR}"/bin/activate if (which ckan > /dev/null); then - ckan -c ${CKAN_INI} run --disable-reloader + ckan -c ${CKAN_INI} run --disable-reloader --threaded else paster serve ${CKAN_INI} fi diff --git a/bin/test-bdd.sh b/bin/test-bdd.sh index 85f5b4e..4b6d548 100755 --- a/bin/test-bdd.sh +++ b/bin/test-bdd.sh @@ -5,5 +5,4 @@ set -ex ahoy install-site -echo "==> Run BDD tests" ahoy test-bdd diff --git a/bin/test-lint.sh b/bin/test-lint.sh index c15afd3..766e142 100755 --- a/bin/test-lint.sh +++ b/bin/test-lint.sh @@ -4,6 +4,5 @@ # set -e -echo "==> Lint code" ahoy lint diff --git a/bin/test.sh b/bin/test.sh index e96a6ff..92ec26a 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -2,7 +2,6 @@ ## # Run tests in CI. # -set -e +set -ex -echo "==> Run Unit tests" ahoy test-unit diff --git a/dev-requirements-2.9-py2.txt b/dev-requirements-2.9-py2.txt deleted file mode 100644 index dcd2993..0000000 --- a/dev-requirements-2.9-py2.txt +++ /dev/null @@ -1,15 +0,0 @@ -behave==1.2.6 -behaving==2.0.0 -Appium-Python-Client<=0.52 -ckanapi==4.3 -ckantoolkit>=0.0.4 -factory-boy -faker==3.0.1 -pytest-factoryboy -flake8==3.8.3 -mock -pytest-ckan -six>=1.13.0 -splinter>=0.13.0,<0.17 - --e git+https://github.com/ckan/ckanext-scheming.git@release-3.0.0#egg=ckanext-scheming diff --git a/dev-requirements-2.9.txt b/dev-requirements-2.9.txt index 5f2cfa0..52b9a01 100644 --- a/dev-requirements-2.9.txt +++ b/dev-requirements-2.9.txt @@ -11,3 +11,4 @@ six>=1.13.0 splinter>=0.13.0,<0.17 -e git+https://github.com/ckan/ckanext-scheming.git@release-3.0.0#egg=ckanext-scheming +zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/docker-compose.yml b/docker-compose.yml index e0be555..1237d19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,8 @@ services: condition: service_healthy solr: condition: service_started + redis: + condition: service_started networks: - amazeeio-network - default @@ -61,7 +63,7 @@ services: - default solr: - image: ckan/ckan-solr:${CKAN_VERSION} + image: ckan/ckan-solr:${CKAN_VERSION}-solr9 ports: - "8983" environment: diff --git a/test/features/resource_availability.feature b/test/features/resource_availability.feature index 1185007..f253c94 100644 --- a/test/features/resource_availability.feature +++ b/test/features/resource_availability.feature @@ -75,7 +75,7 @@ Feature: Re-identification risk governance acknowledgement or Resource visibilit When I log in And I create a dataset and resource with key-value parameters "de_identified_data=NO" and "name=invisible-resource::resource_visible=FALSE" And I press "invisible-resource" - And I press "Manage" + And I press the element with xpath "//a[contains(@href, '/resource/') and contains(@href, '/edit')]" Then I should not see an element with xpath "//label[@for="field-request_privacy_assessment"]//*[@class="control-required"]" And I should see an element with xpath "//select[@id="field-request_privacy_assessment"]//option[@value="" or @value="YES" or @value="NO"]" And I should see "Privacy risk assessment prior to public release might assist the publishing decision-making process" diff --git a/test/features/steps/steps.py b/test/features/steps/steps.py index 5ae2e9b..01b213c 100644 --- a/test/features/steps/steps.py +++ b/test/features/steps/steps.py @@ -32,7 +32,7 @@ def debug_screenshot(context): """ if context.persona and context.persona.get('debug') == 'True': context.execute_steps(u""" - Then I take a screenshot + When I take a screenshot """) @@ -64,7 +64,7 @@ def log_in(context): @when(u'I expand the browser height') def expand_height(context): # Work around x=null bug in Selenium set_window_size - context.browser.driver.set_window_rect(x=0, y=0, width=1024, height=4096) + context.browser.driver.set_window_rect(x=0, y=0, width=1024, height=3072) @when(u'I log in directly') @@ -75,10 +75,11 @@ def log_in_directly(context): :return: """ - assert context.persona, "A persona is required to log in, found [{}] in context. Have you configured the personas in before_scenario?".format(context.persona) + assert context.persona, "A persona is required to log in, found [{}] in context." \ + " Have you configured the personas in before_scenario?".format(context.persona) context.execute_steps(u""" When I attempt to log in with password "$password" - Then I should see an element with xpath "//a[@title='Log out']" + Then I should see an element with xpath "//*[@title='Log out' or @data-bs-title='Log out']/i[contains(@class, 'fa-sign-out')]" """) @@ -125,10 +126,18 @@ def clear_url(context): @when(u'I confirm the dialog containing "{text}" if present') def confirm_dialog_if_present(context, text): - if context.browser.is_text_present(text): - context.execute_steps(u""" - When I press the element with xpath "//*[contains(@class, 'modal-dialog')]//button[contains(@class, 'btn-primary')]" - """) + dialog_xpath = "//*[contains(@class, 'modal-dialog') and contains(string(), '{0}')]".format(text) + if context.browser.is_element_present_by_xpath(dialog_xpath): + parent_xpath = dialog_xpath + elif context.browser.is_text_present(text): + parent_xpath = "//div[contains(string(), '{0}')]/..".format(text) + else: + return + button_xpath = parent_xpath + "//button[contains(@class, 'btn-primary')]" + context.execute_steps(u""" + When I take a debugging screenshot + And I press the element with xpath "{0}" + """.format(button_xpath)) @when(u'I confirm dataset deletion') @@ -159,9 +168,14 @@ def go_to_new_resource_form(context, name): """) else: # Existing dataset, browse to the resource form + if context.browser.is_element_present_by_xpath( + "//a[contains(string(), 'Resources') and contains(@href, '/dataset/resources/')]"): + context.execute_steps(u""" + When I press "Resources" + """) context.execute_steps(u""" - When I press "Resources" - And I press "Add new resource" + When I press "Add new resource" + And I take a debugging screenshot """) @@ -171,6 +185,7 @@ def title_random_text(context): context.execute_steps(u""" When I fill in "title" with "Test Title {0}" And I fill in "name" with "test-title-{0}" if present + And I set "last_generated_title" to "Test Title {0}" And I set "last_generated_name" to "test-title-{0}" """.format(uuid.uuid4())) @@ -186,6 +201,7 @@ def go_to_dataset_page(context): def go_to_dataset(context, name): context.execute_steps(u""" When I visit "/dataset/{0}" + And I take a debugging screenshot """.format(name)) @@ -213,9 +229,11 @@ def select_licence(context, licence_id): @when(u'I enter the resource URL "{url}"') def enter_resource_url(context, url): - context.execute_steps(u""" - When I execute the script "$('#resource-edit [name=url]').val('{0}')" - """.format(url)) + if url != "default": + context.execute_steps(u""" + When I clear the URL field + When I execute the script "$('#resource-edit [name=url]').val('{0}')" + """.format(url)) @when(u'I fill in default dataset fields') @@ -251,7 +269,7 @@ def fill_in_default_link_resource_fields(context): @when(u'I upload "{file_name}" of type "{file_format}" to resource') def upload_file_to_resource(context, file_name, file_format): context.execute_steps(u""" - When I execute the script "button = document.getElementById('resource-upload-button'); if (button) button.click();" + When I execute the script "$('#resource-upload-button').trigger('click');" And I attach the file "{file_name}" to "upload" # Don't quote the injected string since it can have trailing spaces And I execute the script "document.getElementById('field-format').value='{file_format}'" @@ -365,11 +383,16 @@ def _create_dataset_from_params(context, params): When I visit "/dataset/new" And I fill in default dataset fields """) + if 'private' not in params: + params = params + "::private=False" for key, value in _parse_params(params): if key == "name": + # 'name' doesn't need special input, but we want to remember it context.execute_steps(u""" When I set "last_generated_name" to "{0}" """.format(value)) + + # Don't use elif here, we still want to type 'name' as usual if key == "owner_org": # Owner org uses UUIDs as its values, so we need to rely on displayed text context.execute_steps(u""" @@ -436,11 +459,9 @@ def create_resource_from_params(context, resource_params): """) for key, value in _parse_params(resource_params): if key == "url": - if value != "default": - context.execute_steps(u""" - When I clear the URL field - And I execute the script "$('#resource-edit [name=url]').val('{0}')" - """.format(value)) + context.execute_steps(u""" + When I enter the resource URL "{0}" + """.format(value)) elif key == "upload": if value == "default": value = "test_game_data.csv" @@ -489,7 +510,7 @@ def create_resource_from_params(context, resource_params): """.format(key, value)) context.execute_steps(u""" When I take a debugging screenshot - And I press the element with xpath "//form[contains(@class, 'resource-form')]//button[contains(@class, 'btn-primary')]" + And I press the element with xpath "//form[contains(@data-module, 'resource-form')]//button[contains(@class, 'btn-primary')]" And I take a debugging screenshot """) @@ -510,12 +531,8 @@ def filter_contents(mail): payload_bytes = quopri.decodestring(payload) if len(payload_bytes) > 0: payload_bytes += b'=' # do fix the padding error issue - if six.PY2: - decoded_payload = payload_bytes.decode('base64') - else: - import base64 - decoded_payload = six.ensure_text(base64.b64decode(six.ensure_binary(payload_bytes))) - print('decoded_payload: ', decoded_payload) + decoded_payload = six.ensure_text(base64.b64decode(six.ensure_binary(payload_bytes))) + print('Searching for', text, ' and ', text2, ' in decoded_payload: ', decoded_payload) return text in decoded_payload and (not text2 or text2 in decoded_payload) assert context.mail.user_messages(address, filter_contents) @@ -531,7 +548,7 @@ def go_to_admin_config(context): @when(u'I log out') def log_out(context): context.execute_steps(u""" - When I visit "/user/_logout" + When I press the element with xpath "//*[@title='Log out' or @data-bs-title='Log out']" Then I should see "Log in" """) @@ -585,137 +602,3 @@ def click_link_in_email(context, address): url = links[0].rstrip(':') context.browser.visit(url) - - -# ckanext-ytp-comments - - -@when(u'I go to dataset "{name}" comments') -def go_to_dataset_comments(context, name): - context.execute_steps(u""" - When I go to dataset "%s" - And I press "Comments" - """ % (name)) - - -@then(u'I should see the add comment form') -def comment_form_visible(context): - context.execute_steps(u""" - Then I should see an element with xpath "//textarea[@name='comment']" - """) - - -@then(u'I should not see the add comment form') -def comment_form_not_visible(context): - context.execute_steps(u""" - Then I should not see an element with xpath "//input[@name='subject']" - And I should not see an element with xpath "//textarea[@name='comment']" - """) - - -@when(u'I submit a comment with subject "{subject}" and comment "{comment}"') -def submit_comment_with_subject_and_comment(context, subject, comment): - """ - There can be multiple comment forms per page (add, edit, reply) each with fields named "subject" and "comment" - This step overcomes a limitation of the fill() method which only fills a form field by name - :param context: - :param subject: - :param comment: - :return: - """ - context.browser.execute_script(""" - document.querySelector('form#comment_form input[name="subject"]').value = '%s'; - """ % subject) - context.browser.execute_script(""" - document.querySelector('form#comment_form textarea[name="comment"]').value = '%s'; - """ % comment) - context.browser.execute_script(""" - document.querySelector('form#comment_form .form-actions input[type="submit"]').click(); - """) - - -@when(u'I submit a reply with comment "{comment}"') -def submit_reply_with_comment(context, comment): - """ - There can be multiple comment forms per page (add, edit, reply) each with fields named "subject" and "comment" - This step overcomes a limitation of the fill() method which only fills a form field by name - :param context: - :param comment: - :return: - """ - context.browser.execute_script(""" - document.querySelector('.comment-wrapper form textarea[name="comment"]').value = '%s'; - """ % comment) - context.browser.execute_script(""" - document.querySelector('.comment-wrapper form .form-actions input[type="submit"]').click(); - """) - - -# ckanext-qgov - - -@when(u'I lock my account') -def lock_account(context): - context.execute_steps(u""" - When I visit "/user/login" - """) - for x in range(11): - context.execute_steps(u""" - When I attempt to log in with password "incorrect password" - """) - - -# ckanext-datarequests - - -@when(u'I go to the data requests page containing "{keyword}"') -def go_to_datarequest_page_search(context, keyword): - context.execute_steps(u""" - When I visit "/datarequest?q={0}" - """.format(keyword)) - - -@when(u'I go to the data requests page') -def go_to_datarequest_page(context): - context.execute_steps(u""" - When I visit "/datarequest" - """) - - -@when(u'I go to data request "{subject}"') -def go_to_data_request(context, subject): - context.execute_steps(u""" - When I go to the data requests page containing "{0}" - And I click the link with text "{0}" - Then I should see "{0}" within 5 seconds - """.format(subject)) - - -@when(u'I create a datarequest') -def create_datarequest(context): - assert context.persona - context.execute_steps(u""" - When I go to the data requests page - And I press "Add data request" - And I fill in title with random text - And I fill in "description" with "Test description" - And I press the element with xpath "//button[contains(@class, 'btn-primary')]" - """) - - -@when(u'I go to data request "{subject}" comments') -def go_to_data_request_comments(context, subject): - context.execute_steps(u""" - When I go to data request "%s" - And I press "Comments" - """ % (subject)) - - -# ckanext-report - - -@when(u'I go to my reports page') -def go_to_reporting_page(context): - context.execute_steps(u""" - When I visit "/dashboard/reporting" - """)