diff --git a/.github/workflows/oidc-integration-test.yml b/.github/workflows/oidc-integration-test.yml index 2b2919beb6..beb7e2e9a0 100644 --- a/.github/workflows/oidc-integration-test.yml +++ b/.github/workflows/oidc-integration-test.yml @@ -2,8 +2,44 @@ name: "OIDC Integration Test" on: workflow_dispatch: +env: + UBUNTU_VERSION: "24.04" + PYTHON_VERSION: "3.10" jobs: + cache-pyenv-builder: + name: "Cache layers of the pyenv-builder stage" + runs-on: "ubuntu-24.04" + steps: + - name: "Check out repository" + uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2 + - name: "Set up Docker Buildx" + uses: "docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f" # v3.12.0 + with: + driver-opts: | + network=host + - name: "Generate cache key from requirements" + id: cache_key + run: | + echo "requirements_hash=${{ hashFiles('requirements*.txt', 'requirements*.in', 'pyproject.toml') }}" >> $GITHUB_OUTPUT + - name: "Build base layers" + uses: "docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8" # v6.19.2 + with: + context: . + file: ./hack/Dockerfile + target: pyenv-builder + build-args: | + UBUNTU_VERSION=${{ env.UBUNTU_VERSION }} + PYTHON_VERSION=${{ env.PYTHON_VERSION }} + BUILDKIT_INLINE_CACHE=1 + push: false + cache-from: | + type=gha,scope=archivematica-tests-base-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }} + type=gha,scope=archivematica-tests-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }}-${{ steps.cache_key.outputs.requirements_hash }} + cache-to: | + type=gha,scope=archivematica-tests-base-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }},mode=max + type=gha,scope=archivematica-tests-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }}-${{ steps.cache_key.outputs.requirements_hash }},mode=max test: + needs: [cache-pyenv-builder] name: "Test" runs-on: "ubuntu-24.04" steps: @@ -17,8 +53,38 @@ jobs: id: group_id run: | echo "group_id=$(id -g)" >> $GITHUB_OUTPUT + - name: "Generate cache key from requirements" + id: cache_key + run: | + echo "requirements_hash=${{ hashFiles('requirements*.txt', 'requirements*.in', 'pyproject.toml') }}" >> $GITHUB_OUTPUT - name: "Set up buildx" uses: "docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f" # v3.12.0 + with: + driver-opts: | + network=host + - name: "Build integration test image" + uses: "docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8" # v6.19.2 + with: + context: . + file: ./hack/Dockerfile + target: archivematica-dashboard-integration-tests + build-args: | + USER_ID=${{ steps.user_id.outputs.user_id }} + GROUP_ID=${{ steps.group_id.outputs.group_id }} + UBUNTU_VERSION=${{ env.UBUNTU_VERSION }} + PYTHON_VERSION=${{ env.PYTHON_VERSION }} + BUILDKIT_INLINE_CACHE=1 + tags: archivematica-dashboard-integration-tests:latest + push: false + load: true + cache-from: | + type=gha,scope=archivematica-tests-base-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }} + type=gha,scope=archivematica-oidc-integration-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }} + type=gha,scope=archivematica-tests-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }}-${{ steps.cache_key.outputs.requirements_hash }} + cache-to: | + type=gha,scope=archivematica-tests-base-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }},mode=max + type=gha,scope=archivematica-oidc-integration-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }},mode=max + type=gha,scope=archivematica-tests-${{ env.UBUNTU_VERSION }}-${{ env.PYTHON_VERSION }}-${{ steps.cache_key.outputs.requirements_hash }},mode=max - name: "Run tests" run: | ./run.sh @@ -27,8 +93,9 @@ jobs: env: USER_ID: ${{ steps.user_id.outputs.user_id }} GROUP_ID: ${{ steps.group_id.outputs.group_id }} - UBUNTU_VERSION: "24.04" - PYTHON_VERSION: "3.10" + UBUNTU_VERSION: ${{ env.UBUNTU_VERSION }} + PYTHON_VERSION: ${{ env.PYTHON_VERSION }} + SKIP_DOCKER_BUILD: 1 COMPOSE_DOCKER_CLI_BUILD: 1 DOCKER_BUILDKIT: 1 PYTEST_ADDOPTS: -vv diff --git a/hack/Dockerfile b/hack/Dockerfile index 2b3555efc9..7fa9afb79c 100644 --- a/hack/Dockerfile +++ b/hack/Dockerfile @@ -283,6 +283,8 @@ FROM base AS archivematica-tests FROM base AS archivematica-dashboard-integration-tests USER root +ARG USER_ID +ARG GROUP_ID RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ @@ -295,6 +297,9 @@ RUN set -ex \ && mkdir -p /var/archivematica/.cache/ms-playwright \ && python3 -m playwright install firefox +# Copy frontend assets out of where /src is bind-mounted during tests. +COPY --chown=${USER_ID}:${GROUP_ID} --from=archivematica-dashboard-vue-builder --link /src/src/archivematica/dashboard/vue/dist /opt/am-dashboard-dist + # ----------------------------------------------------------------------------- FROM ${TARGET} diff --git a/src/archivematica/dashboard/settings/base.py b/src/archivematica/dashboard/settings/base.py index c2e8db73ca..2b884ed685 100644 --- a/src/archivematica/dashboard/settings/base.py +++ b/src/archivematica/dashboard/settings/base.py @@ -385,6 +385,11 @@ def _get_settings_from_file(path): # Examples: "http://foo.com/static/admin/", "/static/admin/". ADMIN_MEDIA_PREFIX = "/media/" +# Allow tests to serve frontend assets from a path not shadowed by bind mounts over /src. +FRONTEND_DIST_DIR = os.environ.get( + "FRONTEND_DIST_DIR", os.path.join(BASE_PATH, "vue", "dist") +) + # Additional locations of static files STATICFILES_DIRS = ( # Put strings here, like "/home/html/static" or "C:/www/django/static". @@ -394,7 +399,7 @@ def _get_settings_from_file(path): ("css", os.path.join(BASE_PATH, "media", "css")), ("images", os.path.join(BASE_PATH, "media", "images")), ("vendor", os.path.join(BASE_PATH, "media", "vendor")), - ("vue", os.path.join(BASE_PATH, "vue", "dist")), + ("vue", FRONTEND_DIST_DIR), ) # List of finder classes that know how to find static files in diff --git a/tests/integration/docker-compose.yml b/tests/integration/docker-compose.yml index 06886f9bc5..4ec17e5365 100644 --- a/tests/integration/docker-compose.yml +++ b/tests/integration/docker-compose.yml @@ -22,6 +22,7 @@ services: start_period: 15s archivematica-dashboard: + image: "archivematica-dashboard-integration-tests:latest" build: context: "../../" dockerfile: "hack/Dockerfile" @@ -85,6 +86,8 @@ services: OIDC_ACCESS_ATTRIBUTE_MAP_SECONDARY: > {"given_name": "first_name", "family_name": "last_name", "realm_access": "realm_access"} OIDC_RP_SIGN_ALGO: "RS256" + # /src is bind-mounted, so point Django staticfiles to the image-owned dist copy. + FRONTEND_DIST_DIR: "/opt/am-dashboard-dist" volumes: - "../../:/src" depends_on: diff --git a/tests/integration/run.sh b/tests/integration/run.sh index a698b371d1..554c120670 100755 --- a/tests/integration/run.sh +++ b/tests/integration/run.sh @@ -4,12 +4,14 @@ __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd ${__dir} -docker compose build archivematica-dashboard +if [ -z "${SKIP_DOCKER_BUILD}" ]; then + docker compose build archivematica-dashboard -status=$? + status=$? -if [ $status -ne 0 ]; then - exit $status + if [ $status -ne 0 ]; then + exit $status + fi fi docker compose run --rm archivematica-dashboard diff --git a/tests/integration/test_auth.py b/tests/integration/test_auth.py index 592c5d146e..62d148efd6 100644 --- a/tests/integration/test_auth.py +++ b/tests/integration/test_auth.py @@ -5,17 +5,26 @@ from django.contrib.auth.models import AbstractUser from django.urls import reverse from playwright.sync_api import Page +from playwright.sync_api import expect from pytest_django.live_server_helper import LiveServer if "RUN_INTEGRATION_TESTS" not in os.environ: pytest.skip("Skipping integration tests", allow_module_level=True) +def click_logout_from_user_menu(page: Page) -> None: + user_menu = page.locator("li.user.dropdown") + expect(user_menu).to_be_visible() + user_menu.evaluate("node => node.classList.add('open')") + page.get_by_role("button", name="Log out").click() + + @pytest.mark.django_db def test_logout_link_logs_out_user( page: Page, live_server: LiveServer, dashboard_uuid: uuid.UUID, user: AbstractUser ) -> None: page.goto(live_server.url) + assert page.url == f"{live_server.url}{reverse('accounts:login')}" page.get_by_label("Username").fill("foobar") @@ -24,7 +33,6 @@ def test_logout_link_logs_out_user( assert page.url == f"{live_server.url}/transfer/" - page.get_by_text("foobar").click() - page.get_by_role("button", name="Log out").click() + click_logout_from_user_menu(page) assert page.url == f"{live_server.url}{reverse('accounts:login')}" diff --git a/tests/integration/test_oidc_auth.py b/tests/integration/test_oidc_auth.py index e8c37daecb..021f7e7da6 100644 --- a/tests/integration/test_oidc_auth.py +++ b/tests/integration/test_oidc_auth.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import User from django.urls import reverse from playwright.sync_api import Page +from playwright.sync_api import expect from pytest_django.fixtures import SettingsWrapper from pytest_django.live_server_helper import LiveServer @@ -12,6 +13,22 @@ pytest.skip("Skipping integration tests", allow_module_level=True) +def open_user_menu(page: Page) -> None: + user_menu = page.locator("li.user.dropdown") + expect(user_menu).to_be_visible() + user_menu.evaluate("node => node.classList.add('open')") + + +def click_profile_from_user_menu(page: Page) -> None: + open_user_menu(page) + page.get_by_role("link", name="Your profile").click() + + +def click_logout_from_user_menu(page: Page) -> None: + open_user_menu(page) + page.get_by_role("button", name="Log out").click() + + @pytest.mark.django_db def test_oidc_backend_creates_local_user( page: Page, @@ -27,8 +44,7 @@ def test_oidc_backend_creates_local_user( page.get_by_role("button", name="Sign In").click() assert page.url == f"{live_server.url}/transfer/" - page.get_by_text("demo@example.com").click() - page.get_by_role("link", name="Your profile").click() + click_profile_from_user_menu(page) assert page.url == f"{live_server.url}{reverse('accounts:profile')}" assert [ @@ -66,8 +82,7 @@ def test_local_authentication_backend_authenticates_existing_user( assert page.url == f"{live_server.url}/transfer/" - page.get_by_text("foobar").click() - page.get_by_role("link", name="Your profile").click() + click_profile_from_user_menu(page) assert page.url == f"{live_server.url}{reverse('accounts:profile')}" assert [ @@ -147,8 +162,7 @@ def test_setting_request_parameter_in_local_login_url_redirects_to_secondary_pro page.get_by_role("button", name="Sign In").click() assert page.url == f"{live_server.url}/transfer/" - page.get_by_text("supportadmin@example.com").click() - page.get_by_role("link", name="Your profile").click() + click_profile_from_user_menu(page) assert page.url == f"{live_server.url}{reverse('accounts:profile')}" @@ -172,8 +186,7 @@ def test_setting_request_parameter_in_local_login_url_redirects_to_secondary_pro assert page.url == f"{live_server.url}/transfer/" - page.get_by_text("supportdefault@example.com").click() - page.get_by_role("link", name="Your profile").click() + click_profile_from_user_menu(page) assert page.url == f"{live_server.url}{reverse('accounts:profile')}" assert [ @@ -211,8 +224,7 @@ def test_logging_out_logs_out_user_from_secondary_provider_admin_role( assert page.url == f"{live_server.url}/transfer/" # Logging out redirects the user to the login url. - page.get_by_text("supportadmin@example.com").click() - page.get_by_role("button", name="Log out").click() + click_logout_from_user_menu(page) assert page.url == f"{live_server.url}{reverse('accounts:login')}" # Logging in through the OIDC provider requires to authenticate again. @@ -244,8 +256,7 @@ def test_logging_out_logs_out_user_from_secondary_provider_default_role( assert page.url == f"{live_server.url}/transfer/" # Logging out redirects the user to the login url. - page.get_by_text("supportdefault@example.com").click() - page.get_by_role("button", name="Log out").click() + click_logout_from_user_menu(page) assert page.url == f"{live_server.url}{reverse('accounts:login')}" # Logging in through the OIDC provider requires to authenticate again.