From 366cf7ac909609bd332585958185a9f4ae5b3e6e Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Tue, 25 Aug 2026 11:28:58 +0545 Subject: [PATCH 01/16] test: run apiVault on k8s deployment Signed-off-by: Prajwol Amatya --- .github/workflows/acceptance-tests.yml | 2 +- .github/workflows/k8s.yml | 261 ++++++++++++------ tests/config/k8s/README.md | 50 ++++ tests/config/k8s/setup.sh | 4 + tests/config/k8s/templates/extra.tpl | 78 ++++++ .../storageusersvault/deployment.yaml | 161 +++++++++++ .../templates/storageusersvault/services.yaml | 28 ++ tests/config/k8s/values.yaml | 7 + 8 files changed, 512 insertions(+), 79 deletions(-) create mode 100644 tests/config/k8s/templates/storageusersvault/deployment.yaml create mode 100644 tests/config/k8s/templates/storageusersvault/services.yaml diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 84daf2deabd..f5b75fe56e8 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -1,7 +1,7 @@ name: Acceptance Tests on: - pull_request: + # pull_request: workflow_dispatch: jobs: diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index 7dc69d84dc6..9318fbb07c3 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -31,47 +31,48 @@ jobs: fail-fast: false matrix: include: - - suite: apiContract - - suite: apiLocks - - suite: apiSettings - # - suite: apiNotification - - suite: apiCors - - suite: apiGraphUser - - suite: apiGraph - - suite: apiGraphGroup - # - suite: apiSpaces - - suite: apiSpacesShares - - suite: apiSpacesDavOperation - - suite: apiDownloads - - suite: apiAsyncUpload - - suite: apiDepthInfinity - - suite: apiArchiver - - suite: apiActivities - - suite: apiSearch1 - - suite: apiSearch2 - # - suite: apiSearchContent - - suite: apiSharingNgShares - - suite: apiReshare - - suite: apiSharingNgPermissions - - suite: apiSharingNgAdditionalShareRole - - suite: apiSharingNgDriveInvitation - # - suite: apiSharingNgItemInvitation - - suite: apiSharingNgDriveLinkShare - - suite: apiSharingNgItemLinkShare - - suite: apiSharingNgLinkShareManagement - - suite: apiAuthApp - # - suite: apiAntivirus - - suite: apiOcm + # - suite: apiContract + # - suite: apiLocks + # - suite: apiSettings + # # - suite: apiNotification + # - suite: apiCors + # - suite: apiGraphUser + # - suite: apiGraph + # - suite: apiGraphGroup + # # - suite: apiSpaces + # - suite: apiSpacesShares + # - suite: apiSpacesDavOperation + # - suite: apiDownloads + # - suite: apiAsyncUpload + # - suite: apiDepthInfinity + # - suite: apiArchiver + # - suite: apiActivities + # - suite: apiSearch1 + # - suite: apiSearch2 + # # - suite: apiSearchContent + # - suite: apiSharingNgShares + # - suite: apiReshare + # - suite: apiSharingNgPermissions + # - suite: apiSharingNgAdditionalShareRole + # - suite: apiSharingNgDriveInvitation + # # - suite: apiSharingNgItemInvitation + # - suite: apiSharingNgDriveLinkShare + # - suite: apiSharingNgItemLinkShare + # - suite: apiSharingNgLinkShareManagement + # - suite: apiAuthApp + # # - suite: apiAntivirus + # - suite: apiOcm # - suite: apiCollaboration - # - suite: "coreApiAuth,coreApiCapabilities,coreApiFavorites,coreApiMain,coreApiVersions" - - suite: "coreApiShareManagementBasicToShares,coreApiShareManagementToShares" - - suite: "coreApiSharees" - - suite: "coreApiSharePublicLink2" - - suite: "coreApiShareOperationsToShares1,coreApiShareOperationsToShares2,coreApiSharePublicLink1,coreApiShareCreateSpecialToShares1,coreApiShareCreateSpecialToShares2,coreApiShareUpdateToShares" - - suite: "coreApiTrashbin,coreApiTrashbinRestore,coreApiWebdavEtagPropagation1,coreApiWebdavEtagPropagation2" - - suite: "coreApiWebdavDelete,coreApiWebdavOperations,coreApiWebdavMove2" - - suite: "coreApiWebdavProperties" - # - suite: "coreApiWebdavMove1,coreApiWebdavPreviews,coreApiWebdavUpload,coreApiWebdavUploadTUS" + - suite: apiVault + # # - suite: "coreApiAuth,coreApiCapabilities,coreApiFavorites,coreApiMain,coreApiVersions" + # - suite: "coreApiShareManagementBasicToShares,coreApiShareManagementToShares" + # - suite: "coreApiSharees" + # - suite: "coreApiSharePublicLink2" + # - suite: "coreApiShareOperationsToShares1,coreApiShareOperationsToShares2,coreApiSharePublicLink1,coreApiShareCreateSpecialToShares1,coreApiShareCreateSpecialToShares2,coreApiShareUpdateToShares" + # - suite: "coreApiTrashbin,coreApiTrashbinRestore,coreApiWebdavEtagPropagation1,coreApiWebdavEtagPropagation2" + # - suite: "coreApiWebdavDelete,coreApiWebdavOperations,coreApiWebdavMove2" + # - suite: "coreApiWebdavProperties" + # # - suite: "coreApiWebdavMove1,coreApiWebdavPreviews,coreApiWebdavUpload,coreApiWebdavUploadTUS" steps: - name: Checkout @@ -103,30 +104,32 @@ jobs: - name: Prepare hosts run: | - echo "127.0.0.1 ocis-server clamav email collabora onlyoffice fakeoffice tika federation-ocis-server" \ + echo "127.0.0.1 ocis-server clamav email collabora onlyoffice fakeoffice tika federation-ocis-server keycloak" \ | sudo tee -a /etc/hosts - name: Spin up K3d Cluster run: make -C tests/config/k8s create-cluster - - name: Prepare Helm Charts & Deploy oCIS + - name: Prepare Helm Charts env: ENABLE_ANTIVIRUS: ${{ matrix.suite == 'apiAntivirus' }} ENABLE_EMAIL: ${{ matrix.suite == 'apiNotification' || matrix.suite == 'apiSettings' || matrix.suite == 'apiOcm' }} - ENABLE_TIKA: ${{ matrix.suite == 'apiSearchContent' }} + ENABLE_TIKA: ${{ matrix.suite == 'apiSearchContent' || matrix.suite == 'apiVault' }} ENABLE_WOPI: ${{ matrix.suite == 'apiCollaboration' }} ENABLE_OCM: ${{ matrix.suite == 'apiOcm' }} ENABLE_AUTH_APP: ${{ matrix.suite == 'apiAuthApp' }} + ENABLE_VAULT: ${{ matrix.suite == 'apiVault' }} run: | cd tests/config/k8s make prepare-charts - kubectl get pods -n ocis-server -Aw & - make deploy-ocis if [[ "${{ matrix.suite }}" == "apiOcm" ]]; then OCM=true make prepare-charts - OCM=true make deploy-ocis fi + # Keycloak/postgres (and the other suite-specific backends below) must be up and + # exposed to the cluster *before* oCIS is deployed: the proxy validates the + # OCIS_OIDC_ISSUER well-known endpoint against Keycloak at startup in vault mode, + # so `helm install --wait` would time out waiting for the proxy pod otherwise. - name: Deploy Suite-Specific External Backends run: | if [[ "${{ matrix.suite }}" == "apiNotification" || \ @@ -147,13 +150,80 @@ jobs: bash tests/config/k8s/expose-external-svc.sh clamav:3310 fi - if [[ "${{ matrix.suite }}" == "apiSearchContent" ]]; then + if [[ "${{ matrix.suite }}" == "apiSearchContent" || "${{ matrix.suite }}" == "apiVault" ]]; then docker run -d \ -p 9998:9998 \ --name tika \ apache/tika:3.2.2.0-full bash tests/config/k8s/expose-external-svc.sh tika:9998 fi + + if [[ "${{ matrix.suite }}" == "apiVault" ]]; then + # GitHub runners ship PostgreSQL pre-started on 5432; stop it so our + # container (needed by keycloak) can bind the same port. + sudo systemctl stop postgresql || true + + mkdir -p keycloak-certs + openssl req -x509 -newkey rsa:2048 \ + -keyout keycloak-certs/keycloakkey.pem \ + -out keycloak-certs/keycloakcrt.pem \ + -nodes -days 365 -subj "/CN=keycloak" + chmod 777 keycloak-certs/* + + # patch the realm so the "web" client's redirect/origin URLs match + # the k8s ingress domain instead of the non-k8s "localhost:9200" one + sed 's|https://localhost:9200|https://ocis-server|g' \ + tests/config/ci/ocis-mfa-ci-realm.dist.json > /tmp/ocis-realm.json + + docker run -d --name postgres --network host \ + -e POSTGRES_DB=keycloak \ + -e POSTGRES_USER=keycloak \ + -e POSTGRES_PASSWORD=keycloak \ + postgres:alpine3.18 + + for i in {1..30}; do + docker exec postgres pg_isready -U keycloak && break + echo "Waiting for postgres... ($i/30)" + sleep 2 + done + + docker run -d --name keycloak --network host \ + -e OCIS_DOMAIN=https://ocis-server \ + -e KC_HOSTNAME=keycloak \ + -e KC_PORT=8443 \ + -e KC_DB=postgres \ + -e KC_DB_URL=jdbc:postgresql://localhost:5432/keycloak \ + -e KC_DB_USERNAME=keycloak \ + -e KC_DB_PASSWORD=keycloak \ + -e KC_FEATURES=impersonation \ + -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \ + -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \ + -e KC_HTTPS_CERTIFICATE_FILE=/keycloak-certs/keycloakcrt.pem \ + -e KC_HTTPS_CERTIFICATE_KEY_FILE=/keycloak-certs/keycloakkey.pem \ + -v "$(pwd)/keycloak-certs:/keycloak-certs:ro" \ + -v /tmp/ocis-realm.json:/opt/keycloak/data/import/ocis-mfa-ci-realm.dist.json:ro \ + quay.io/keycloak/keycloak:26.5.6 \ + start-dev --proxy-headers xforwarded \ + --spi-connections-http-client-default-disable-trust-manager=true \ + --import-realm --health-enabled=true + + for i in {1..60}; do + curl -skf https://localhost:9000/health/ready && break + echo "Waiting for keycloak... ($i/60)" + sleep 5 + done + + bash tests/config/k8s/expose-external-svc.sh keycloak:8443 + fi + + - name: Deploy oCIS + run: | + cd tests/config/k8s + kubectl get pods -n ocis-server -Aw & + make deploy-ocis + if [[ "${{ matrix.suite }}" == "apiOcm" ]]; then + OCM=true make deploy-ocis + fi - name: Wait for oCIS to be ready env: @@ -175,45 +245,67 @@ jobs: kubectl get pods -n ocis-server - echo "Creating test file in oCIS..." - - request() { - local method=$1 - shift - curl -ks -o /dev/null -w "%{http_code}" \ - -X "$method" \ - -u admin:admin \ - "$@" \ - "$FILE_URL" - } - - retry_http() { - local expected=$1 + retry() { + local label=$1 shift for i in {1..30}; do - status=$("$@") - if [ "$status" = "$expected" ]; then - echo "Succeeded (HTTP $status)" + if "$@"; then + echo "$label succeeded" return 0 fi - echo "Attempt $i failed (HTTP $status), retrying in 10s..." + echo "$label attempt $i failed, retrying in 10s..." sleep 10 done - echo "Failed after 30 attempts" + echo "$label failed after 30 attempts" return 1 } - put_file() { - request PUT \ - -H "Content-Type: text/plain" \ - --data "Hello from GitHub Actions!" - } - delete_file() { - request DELETE - } - echo "Creating test file..." - retry_http 201 put_file || exit 1 - echo "Deleting test file..." - retry_http 204 delete_file || exit 1 + + if [[ "${{ matrix.suite }}" == "apiVault" ]]; then + # In vault mode IDM_CREATE_DEMO_USERS=false, so there is no "admin" + # LDAP user to authenticate the basic-auth check below against. + # Poll the proxy's unauthenticated debug readyz endpoint instead, + # reached via port-forward since it isn't exposed by any Service. + # local port 19205 (not 9205): 9100-9399 is already bound on the + # host by the k3d loadbalancer's NodePort range, see create-cluster. + kubectl -n ocis-server port-forward deployment/proxy 19205:9205 & + PORT_FORWARD_PID=$! + trap 'kill $PORT_FORWARD_PID 2>/dev/null' EXIT + + proxy_ready() { + curl -sf http://localhost:19205/readyz > /dev/null + } + retry "proxy readyz" proxy_ready || exit 1 + else + echo "Creating test file in oCIS..." + + request() { + local method=$1 + shift + curl -ks -o /dev/null -w "%{http_code}" \ + -X "$method" \ + -u admin:admin \ + "$@" \ + "$FILE_URL" + } + put_file() { + request PUT \ + -H "Content-Type: text/plain" \ + --data "Hello from GitHub Actions!" + } + delete_file() { + request DELETE + } + check_status() { + local expected=$1 + shift + status=$("$@") + [ "$status" = "$expected" ] + } + echo "Creating test file..." + retry "create file (HTTP 201)" check_status 201 put_file || exit 1 + echo "Deleting test file..." + retry "delete file (HTTP 204)" check_status 204 delete_file || exit 1 + fi - name: Expose debug ports run: bash tests/config/k8s/expose-debug-svc.sh @@ -230,6 +322,17 @@ jobs: --skip-ocis-run \ -n ocis-server & + - name: Install Playwright for vault tests + if: matrix.suite == 'apiVault' + run: | + composer install --no-progress + composer bin behat install --no-progress + vendor-php/bin/playwright-install + vendor-php/bin/playwright-install --browsers + env: + COMPOSER_NO_INTERACTION: "1" + COMPOSER_NO_AUDIT: "1" + - name: Prepare expected failures if: startsWith( matrix.suite, 'core' ) env: @@ -253,6 +356,8 @@ jobs: OCIS_WRAPPER_URL: ${{ env.OCIS_WRAPPER_URL }} COLLABORATION_SERVICE_URL: http://ocis-server:9304 K8S: ${{ env.K8S }} + KEYCLOAK: ${{ matrix.suite == 'apiVault' }} + KC_URL: https://keycloak:8443 run: make test-acceptance-api - name: Run Core ${{ matrix.suite }} tests diff --git a/tests/config/k8s/README.md b/tests/config/k8s/README.md index 3f8bf5a7648..1fdc7a09538 100644 --- a/tests/config/k8s/README.md +++ b/tests/config/k8s/README.md @@ -57,6 +57,7 @@ > - `ENABLE_WOPI=true`: WOPI test suites > - `ENABLE_OCM=true`: OCM test suites > - `ENABLE_AUTH_APP=true`: auth-app test suites + > - `ENABLE_VAULT=true`: Vault test suites (needs `ENABLE_TIKA=true` too) > > ⚠️ When using the above environment variables, > make sure you run the necessary external services and expose them to the cluster. @@ -218,6 +219,55 @@ in a separate namespace on the same cluster, alongside the ocis server: make test-acceptance-api ``` +### Run Vault tests + +Vault mode requires Keycloak (as an external OIDC provider that can assert MFA/acr +claims) backed by postgres, plus Tika for full text search: + +1. Check if setup [step 3](#deploy-ocis-in-k8s) is done correctly. (`ENABLE_VAULT=true ENABLE_TIKA=true`) +2. Start postgres and Keycloak (self-signed cert, `CN=keycloak`, importing + `tests/config/ci/ocis-mfa-ci-realm.dist.json` with `https://localhost:9200` + replaced by `https://ocis-server`), and start tika: + + ```bash + docker run -d --name postgres --network host \ + -e POSTGRES_DB=keycloak -e POSTGRES_USER=keycloak -e POSTGRES_PASSWORD=keycloak \ + postgres:alpine3.18 + + docker run -d --name keycloak --network host \ + -e OCIS_DOMAIN=https://ocis-server -e KC_HOSTNAME=keycloak -e KC_PORT=8443 \ + -e KC_DB=postgres -e KC_DB_URL=jdbc:postgresql://localhost:5432/keycloak \ + -e KC_DB_USERNAME=keycloak -e KC_DB_PASSWORD=keycloak \ + -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \ + -e KC_HTTPS_CERTIFICATE_FILE=/keycloak-certs/keycloakcrt.pem \ + -e KC_HTTPS_CERTIFICATE_KEY_FILE=/keycloak-certs/keycloakkey.pem \ + -v "$(pwd)/keycloak-certs:/keycloak-certs:ro" \ + -v /tmp/ocis-realm.json:/opt/keycloak/data/import/ocis-mfa-ci-realm.dist.json:ro \ + quay.io/keycloak/keycloak:26.5.6 \ + start-dev --import-realm --health-enabled=true + + docker run -d -p 9998:9998 --name tika apache/tika:3.2.2.0-full + ``` + +3. Expose them to the cluster + + ```bash + bash tests/config/k8s/expose-external-svc.sh keycloak:8443 + bash tests/config/k8s/expose-external-svc.sh tika:9998 + ``` + +4. Run the tests (Playwright is required for the `@javascript`/web-UI-login + scenarios; run `vendor-php/bin/playwright-install --browsers` once first) + + ```bash + TEST_SERVER_URL=https://ocis-server \ + K8S=true \ + KEYCLOAK=true \ + KC_URL=https://keycloak:8443 \ + BEHAT_FEATURE=/apiVault/vault.feature \ + make test-acceptance-api + ``` + ## Cleanup the Setup To delete the cluster and all the setup resources, run the following command: diff --git a/tests/config/k8s/setup.sh b/tests/config/k8s/setup.sh index d8f5fa2b08c..5a95043ea14 100644 --- a/tests/config/k8s/setup.sh +++ b/tests/config/k8s/setup.sh @@ -79,5 +79,9 @@ if [[ "$ENABLE_AUTH_APP" == "true" ]]; then sed -i '/authapp:/{n;s|false|true|}' $CFG_DIR/values.yaml fi +if [[ "$ENABLE_VAULT" == "true" ]]; then + sed -i '/vault:/{n;s|false|true|}' $CFG_DIR/values.yaml +fi + # copy custom values file cp $CFG_DIR/values.yaml "$CHT_DIR/ci/deployment-values.yaml" diff --git a/tests/config/k8s/templates/extra.tpl b/tests/config/k8s/templates/extra.tpl index a1327e675e9..63975593389 100644 --- a/tests/config/k8s/templates/extra.tpl +++ b/tests/config/k8s/templates/extra.tpl @@ -50,4 +50,82 @@ - name: ANTIVIRUS_CLAMAV_SOCKET value: "tcp://clamav:3310" {{- end -}} +{{- if .Values.features.vault.enabled }} +{{- if eq .appName "proxy" }} +- name: OCIS_ENABLE_VAULT_MODE + value: "true" +- name: OCIS_MFA_ENABLED + value: "true" +- name: PROXY_OIDC_ISSUER + value: "https://keycloak:8443/realms/oCIS" +- name: PROXY_OIDC_REWRITE_WELLKNOWN + value: "true" +- name: PROXY_AUTOPROVISION_ACCOUNTS + value: "true" +- name: PROXY_ROLE_ASSIGNMENT_DRIVER + value: oidc +- name: PROXY_USER_OIDC_CLAIM + value: preferred_username +- name: PROXY_USER_CS3_CLAIM + value: username +{{- end -}} +{{- if eq .appName "frontend" }} +- name: OCIS_ENABLE_VAULT_MODE + value: "true" +- name: OCIS_MFA_ENABLED + value: "true" +{{- end -}} +{{- if eq .appName "graph" }} +- name: OCIS_ENABLE_VAULT_MODE + value: "true" +- name: GRAPH_ASSIGN_DEFAULT_USER_ROLE + value: "false" +- name: GRAPH_USERNAME_MATCH + value: none +{{- end -}} +{{- if eq .appName "gateway" }} +- name: OCIS_ENABLE_VAULT_MODE + value: "true" +{{- end -}} +{{- if eq .appName "web" }} +{{/* WEB_OIDC_AUTHORITY intentionally left as the chart default (the oCIS + domain): PROXY_OIDC_REWRITE_WELLKNOWN transparently proxies the + well-known document from Keycloak under that same origin, matching + how the non-k8s vault test setup (run-github.py) configures this. */}} +- name: WEB_OIDC_CLIENT_ID + value: web +- name: WEB_OIDC_SCOPE + value: "openid profile email acr" +{{- end -}} +{{- if eq .appName "webfinger" }} +- name: WEBFINGER_OIDC_ISSUER + value: "https://keycloak:8443/realms/oCIS" +{{- end -}} +{{- if eq .appName "users" }} +- name: USERS_IDP_URL + value: "https://keycloak:8443/realms/oCIS" +{{- end -}} +{{- if eq .appName "groups" }} +- name: GROUPS_IDP_URL + value: "https://keycloak:8443/realms/oCIS" +{{- end -}} +{{- if eq .appName "ocs" }} +- name: OCS_IDM_ADDRESS + value: "https://keycloak:8443/realms/oCIS" +{{- end -}} +{{- if eq .appName "idm" }} +- name: OCIS_OIDC_ISSUER + value: "https://keycloak:8443/realms/oCIS" +- name: IDM_CREATE_DEMO_USERS + value: "false" +{{- end -}} +{{- if eq .appName "storageusers-vault" }} +- name: STORAGE_USERS_ENABLE_VAULT_MODE + value: "true" +- name: STORAGE_USERS_SERVICE_NAME + value: storage-users-vault +- name: STORAGE_USERS_EVENTS_CONSUMER_GROUP + value: vault-dcfs +{{- end -}} +{{- end -}} {{- end -}} diff --git a/tests/config/k8s/templates/storageusersvault/deployment.yaml b/tests/config/k8s/templates/storageusersvault/deployment.yaml new file mode 100644 index 00000000000..77e68c8f7d0 --- /dev/null +++ b/tests/config/k8s/templates/storageusersvault/deployment.yaml @@ -0,0 +1,161 @@ +{{- if .Values.features.vault.enabled }} +{{- include "ocis.basicServiceTemplates" (dict "scope" . "appName" "appNameStorageUsers" "appNameSuffix" "vault") -}} +apiVersion: apps/v1 +kind: Deployment +{{ include "ocis.metadata" . }} +spec: + {{- include "ocis.selector" . | nindent 2 }} + {{- if and (not .Values.autoscaling.enabled) (.Values.replicas) }} + replicas: {{ .Values.replicas }} + {{- end }} + {{- include "ocis.deploymentStrategy" . | nindent 2 }} + template: + {{- include "ocis.templateMetadata" (dict "scope" $ "configCheck" false) | nindent 4 }} + spec: + {{- include "ocis.affinity" $ | nindent 6 }} + {{- include "ocis.securityContextAndtopologySpreadConstraints" . | nindent 6 }} + {{- include "ocis.priorityClassName" $.priorityClassName | nindent 6 }} + {{- include "ocis.hostAliases" $ | nindent 6 }} + nodeSelector: {{ toYaml $.nodeSelector | nindent 8 }} + containers: + - name: {{ .appName }} + {{- include "ocis.image" $ | nindent 10 }} + command: ["ocis"] + args: ["storage-users", "server"] + {{- include "ocis.containerSecurityContext" . | nindent 10 }} + env: + {{- include "ocis.serviceRegistry" . | nindent 12 }} + {{- include "ocis.events" . | nindent 12 }} + {{- include "ocis.cacheStore" . | nindent 12 }} + {{- include "ocis.cors" . | nindent 12 }} + + # this is a secondary storage-users instance, dedicated to vault storage + # see tests/config/k8s/templates/extra.tpl for STORAGE_USERS_ENABLE_VAULT_MODE + # and STORAGE_USERS_SERVICE_NAME + + - name: STORAGE_USERS_GATEWAY_GRPC_ADDR + value: {{ .appNameGateway }}:9142 + + - name: STORAGE_USERS_LOG_COLOR + value: {{ .Values.logging.color | quote }} + - name: STORAGE_USERS_LOG_LEVEL + value: {{ .Values.logging.level | quote }} + - name: STORAGE_USERS_LOG_PRETTY + value: {{ .Values.logging.pretty | quote }} + + - name: STORAGE_USERS_TRACING_ENABLED + value: "{{ .Values.tracing.enabled }}" + - name: STORAGE_USERS_TRACING_TYPE + value: {{ .Values.tracing.type | quote }} + - name: STORAGE_USERS_TRACING_ENDPOINT + value: {{ .Values.tracing.endpoint | quote }} + - name: STORAGE_USERS_TRACING_COLLECTOR + value: {{ .Values.tracing.collector | quote }} + + - name: STORAGE_USERS_DEBUG_PPROF + value: {{ .Values.debug.profiling | quote }} + + - name: STORAGE_USERS_GRPC_ADDR + value: 0.0.0.0:9157 + - name: STORAGE_USERS_DEBUG_ADDR + value: 0.0.0.0:9159 + + - name: STORAGE_USERS_HTTP_ADDR + value: 0.0.0.0:9158 + - name: STORAGE_USERS_DATA_SERVER_URL + value: "http://{{ .appName }}:9158/data" + + - name: STORAGE_USERS_DRIVER + value: ocis + - name: STORAGE_USERS_OCIS_MAX_CONCURRENCY + value: {{ .Values.services.storageusers.storageBackend.driverConfig.ocis.maxConcurrency | quote }} + + - name: STORAGE_USERS_UPLOAD_EXPIRATION + value: {{ .Values.services.storageusers.maintenance.cleanUpExpiredUploads.uploadExpiration | quote }} + + - name: STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE + value: {{ .Values.services.storageusers.maintenance.purgeExpiredTrashBinItems.personalDeleteBefore | quote}} + - name: STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE + value: {{ .Values.services.storageusers.maintenance.purgeExpiredTrashBinItems.projectDeleteBefore | quote }} + + - name: STORAGE_USERS_SERVICE_ACCOUNT_ID + valueFrom: + configMapKeyRef: + name: {{ include "config.authService" . }} + key: service-account-id + - name: STORAGE_USERS_SERVICE_ACCOUNT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "secrets.serviceAccountSecret" . }} + key: service-account-secret + + - name: STORAGE_USERS_STAT_CACHE_STORE + value: noop + + - name: STORAGE_USERS_MOUNT_ID + valueFrom: + configMapKeyRef: + name: {{ include "config.storageUsers" . }} + key: storage-uuid + + - name: STORAGE_USERS_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "secrets.jwtSecret" . }} + key: jwt-secret + + - name: OCIS_TRANSFER_SECRET + valueFrom: + secretKeyRef: + name: {{ include "secrets.transferSecret" . }} + key: transfer-secret + + - name: OCIS_ASYNC_UPLOADS + value: "true" + - name: STORAGE_USERS_EVENTS_NUM_CONSUMERS + value: {{ .Values.services.storageusers.events.consumer.concurrency | quote }} + + - name: STORAGE_USERS_DATA_GATEWAY_URL + value: "http://{{ .appNameFrontend }}:9140/data/" + + {{- include "ocis.caEnv" $ | nindent 12}} + {{- include "ocis.extraEnvs" . | nindent 12}} + + {{- include "ocis.livenessProbe" . | nindent 10 }} + + resources: {{ toYaml .resources | nindent 12 }} + + ports: + - name: grpc + containerPort: 9157 + - name: http + containerPort: 9158 + - name: metrics-debug + containerPort: 9159 + + volumeMounts: + - name: tmp-volume + mountPath: /tmp + - name: messaging-system-ca + mountPath: /etc/ocis/messaging-system-ca + readOnly: true + - name: {{ include "ocis.persistence.dataVolumeName" . }} + mountPath: /var/lib/ocis + {{- include "ocis.caPath" $ | nindent 12}} + {{- include "ocis.extraVolMounts" . | nindent 12}} + + {{- include "ocis.imagePullSecrets" $ | nindent 6 }} + volumes: + - name: tmp-volume + emptyDir: {} + - name: messaging-system-ca + {{ if and (.Values.messagingSystem.external.enabled) (not .Values.messagingSystem.external.tls.certTrusted) }} + secret: + secretName: {{ include "secrets.messagingSystemCASecret" . }} + {{ else }} + emptyDir: {} + {{ end }} + {{- include "ocis.caVolume" $ | nindent 8}} + {{- include "ocis.persistence.dataVolume" . | nindent 8 }} + {{- include "ocis.extraVolumes" . | nindent 8}} +{{- end -}} diff --git a/tests/config/k8s/templates/storageusersvault/services.yaml b/tests/config/k8s/templates/storageusersvault/services.yaml new file mode 100644 index 00000000000..c6b7549d977 --- /dev/null +++ b/tests/config/k8s/templates/storageusersvault/services.yaml @@ -0,0 +1,28 @@ +{{- if .Values.features.vault.enabled }} +{{- include "ocis.basicServiceTemplates" (dict "scope" . "appName" "appNameStorageUsers" "appNameSuffix" "vault") -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ .appName }} + namespace: {{ template "ocis.namespace" . }} + labels: + app: {{ .appName }} + ocis-metrics: enabled + {{- include "ocis.labels" . | nindent 4 }} +spec: + selector: + app: {{ .appName }} + ports: + - name: grpc + port: 9157 + protocol: TCP + appProtocol: {{ .Values.service.appProtocol.grpc | quote}} + - name: http + port: 9158 + protocol: TCP + appProtocol: {{ .Values.service.appProtocol.http | quote}} + - name: metrics-debug + port: 9159 + protocol: TCP + appProtocol: {{ .Values.service.appProtocol.http | quote}} +{{- end -}} diff --git a/tests/config/k8s/values.yaml b/tests/config/k8s/values.yaml index f999e3bb88a..5b4c69ec660 100644 --- a/tests/config/k8s/values.yaml +++ b/tests/config/k8s/values.yaml @@ -16,6 +16,8 @@ insecure: features: authapp: enabled: false + vault: + enabled: false emailNotifications: enabled: false smtp: @@ -95,6 +97,7 @@ http: - 'blob:' - 'https://raw.githubusercontent.com/owncloud/awesome-ocis/' - 'https://marketplace.owncloud.com/' + - 'https://keycloak:8443/' defaultSrc: - "'none'" fontSrc: @@ -106,6 +109,7 @@ http: - "'self'" - 'blob:' - 'https://embed.diagrams.net/' + - 'https://keycloak:8443/' imgSrc: - "'self'" - 'data:' @@ -147,6 +151,9 @@ services: enabled: true accessModes: - ReadWriteOnce + storageusers-vault: + persistence: + enabled: false ocm: persistence: enabled: true From 5162a57155bd6cfca10f53f6165c64ffe40bc291 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Tue, 25 Aug 2026 14:47:21 +0545 Subject: [PATCH 02/16] test: use oc_oAuth.user key to get token Signed-off-by: Prajwol Amatya --- tests/acceptance/TestHelpers/GraphHelper.php | 6 ++++- tests/acceptance/bootstrap/Provisioning.php | 25 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/TestHelpers/GraphHelper.php b/tests/acceptance/TestHelpers/GraphHelper.php index 4c479186177..72a750b4262 100644 --- a/tests/acceptance/TestHelpers/GraphHelper.php +++ b/tests/acceptance/TestHelpers/GraphHelper.php @@ -1004,8 +1004,12 @@ public static function getMySpaces( ): ResponseInterface { $urlArguments = $urlArguments ? "?$urlArguments" : ""; $url = self::getFullUrl($baseUrl, "me/drives" . $urlArguments, $isVault); + var_dump($url); - return HttpRequestHelper::get($url, $user, $password, $headers, $body); + $response = HttpRequestHelper::get($url, $user, $password, $headers, $body); + var_dump($response->getStatusCode()); + var_dump($response->getBody()->getContents()); + return $response; } /** diff --git a/tests/acceptance/bootstrap/Provisioning.php b/tests/acceptance/bootstrap/Provisioning.php index dfb4bb9c775..4e2a67c045c 100644 --- a/tests/acceptance/bootstrap/Provisioning.php +++ b/tests/acceptance/bootstrap/Provisioning.php @@ -81,6 +81,27 @@ public function getOcisUserToken(string $userId): array { return $this->userTokens[$userId]; } + /** + * Finds the oidc-client-ts user entry (key "oc_oAuth.user::") + * in a Playwright browser storage state and decodes its token data. + * The exact position of this entry among the other localStorage keys is not + * guaranteed, so it must be located by name rather than by a fixed index. + * + * @param array $state + * + * @return mixed + * @throws Exception + */ + public function extractOidcTokenDataFromStorageState(array $state): mixed { + $localStorage = $state['origins'][0]['localStorage'] ?? []; + foreach ($localStorage as $entry) { + if (\str_starts_with($entry['name'] ?? '', 'oc_oAuth.user:')) { + return \json_decode($entry['value']); + } + } + throw new Exception('Could not find an "oc_oAuth.user:" entry in the browser storage state.'); + } + /** * Check if this is the admin group. That group is always a local group in * ownCloud10, even if other groups come from LDAP. @@ -711,7 +732,7 @@ public function setAccessTokenForAdmin(): void { $adminUser["actualUsername"], $adminUser["password"], ); - $tokenData = \json_decode($state['origins'][0]['localStorage'][2]['value']); + $tokenData = $this->extractOidcTokenDataFromStorageState($state); $this->setOcisUserToken($adminUser, $tokenData); } @@ -735,7 +756,7 @@ public function userHasLoggedInViaWebUI(string $user): void { $userAttribute["actualUsername"], $userAttribute["password"], ); - $stateData = \json_decode($state['origins'][0]['localStorage'][2]['value']); + $stateData = $this->extractOidcTokenDataFromStorageState($state); $this->setOcisUserToken($userAttribute, $stateData); $response = $this->graphContext->adminHasRetrievedUserUsingTheGraphApi($user); $userAttribute['id'] = $this->getJsonDecodedResponse($response)['id']; From 1f463e588bc750eb5014713d20926d05f86804eb Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Wed, 26 Aug 2026 14:48:11 +0545 Subject: [PATCH 03/16] test: debug storage-users-vault Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 28 + tests/acceptance/TestHelpers/GraphHelper.php | 1 + tests/acceptance/bootstrap/Provisioning.php | 14 +- .../features/apiVault/vault.feature | 2146 ++++++++--------- 4 files changed, 1115 insertions(+), 1074 deletions(-) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index 9318fbb07c3..c254771d88c 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -307,6 +307,34 @@ jobs: retry "delete file (HTTP 204)" check_status 204 delete_file || exit 1 fi + - name: Debug storage-users-vault + if: always() && matrix.suite == 'apiVault' + run: | + echo "=== deployment/pods (label app=storageusers-vault) ===" + kubectl get deployment,pods -n ocis-server -l app=storageusers-vault -o wide || true + + echo "=== describe deployment storageusers-vault ===" + kubectl -n ocis-server describe deployment storageusers-vault || true + + echo "=== service storageusers-vault ===" + kubectl get svc storageusers-vault -n ocis-server -o wide || true + + echo "=== kubectl logs (likely empty: OCIS_LOG_FILE redirects to a file, not stdout) ===" + kubectl -n ocis-server logs deployment/storageusers-vault --tail=200 || true + kubectl -n ocis-server logs deployment/storageusers-vault --previous --tail=200 || true + + echo "=== real logs: every service in this chart writes to the same hostPath-mounted file ===" + echo "=== tests/config/k8s/logs/ocis.log (mounted into the k3d node at /logs, see create-cluster) ===" + ls -la tests/config/k8s/logs/ || true + echo "--- lines mentioning storage-users-vault, panic, fatal, or error ---" + grep -iE "storage-users-vault|panic|fatal|error" tests/config/k8s/logs/*.log 2>/dev/null | tail -200 || true + + echo "=== gateway lines mentioning vault ===" + grep -i "vault" tests/config/k8s/logs/*.log 2>/dev/null | grep -i gateway | tail -100 || true + + echo "=== features.vault.enabled in the values file actually used to deploy ===" + grep -A1 "vault:" tests/config/k8s/values.yaml || true + - name: Expose debug ports run: bash tests/config/k8s/expose-debug-svc.sh diff --git a/tests/acceptance/TestHelpers/GraphHelper.php b/tests/acceptance/TestHelpers/GraphHelper.php index 72a750b4262..0715db0c639 100644 --- a/tests/acceptance/TestHelpers/GraphHelper.php +++ b/tests/acceptance/TestHelpers/GraphHelper.php @@ -1007,6 +1007,7 @@ public static function getMySpaces( var_dump($url); $response = HttpRequestHelper::get($url, $user, $password, $headers, $body); + var_dump($headers); var_dump($response->getStatusCode()); var_dump($response->getBody()->getContents()); return $response; diff --git a/tests/acceptance/bootstrap/Provisioning.php b/tests/acceptance/bootstrap/Provisioning.php index 4e2a67c045c..f218cfaeaa9 100644 --- a/tests/acceptance/bootstrap/Provisioning.php +++ b/tests/acceptance/bootstrap/Provisioning.php @@ -96,7 +96,19 @@ public function extractOidcTokenDataFromStorageState(array $state): mixed { $localStorage = $state['origins'][0]['localStorage'] ?? []; foreach ($localStorage as $entry) { if (\str_starts_with($entry['name'] ?? '', 'oc_oAuth.user:')) { - return \json_decode($entry['value']); + $tokenData = \json_decode($entry['value']); + // TEMPORARY DEBUG: dump the access token's claims (acr in particular) to + // check whether the vault/MFA login flow actually elevates the session. + $accessToken = $tokenData->access_token ?? null; + if (\is_string($accessToken)) { + $segments = \explode('.', $accessToken); + if (\count($segments) === 3) { + $payload = \strtr($segments[1], '-_', '+/'); + $payload .= \str_repeat('=', (4 - \strlen($payload) % 4) % 4); + echo "DEBUG access token claims: " . \base64_decode($payload) . "\n"; + } + } + return $tokenData; } } throw new Exception('Could not find an "oc_oAuth.user:" entry in the browser storage state.'); diff --git a/tests/acceptance/features/apiVault/vault.feature b/tests/acceptance/features/apiVault/vault.feature index ad60348f956..0785f78446f 100644 --- a/tests/acceptance/features/apiVault/vault.feature +++ b/tests/acceptance/features/apiVault/vault.feature @@ -21,1079 +21,1079 @@ Feature: vault | vaultFile.txt | - Scenario: user can create folders and files in project space in vault - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Alice" has created a space "vault-space" in vault with the default quota using the Graph API - When user "Alice" creates a folder "vaultFolder" in space "vault-space" in vault using the WebDav Api - Then the HTTP status code should be "201" - When user "Alice" uploads a file inside space "vault-space" with content "some content" to "vaultFile.txt" in vault using the WebDAV API - Then the HTTP status code should be "201" - And for user "Alice" the space "vault-space" in vault should contain these entries: - | vaultFolder | - | vaultFile.txt | - - - Scenario: resources in drive and vault are isolated - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "driveFolder" in space "Personal" - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFile.txt" - And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API - Then the HTTP status code should be "201" - And for user "Alice" the space "Personal" in vault should contain these entries: - | vaultFolder | - | vaultFile.txt | - And for user "Alice" the space "Personal" should contain these entries: - | driveFolder | - | driveFile.txt | - And for user "Alice" the space "Personal" in vault should not contain these entries: - | driveFolder | - | driveFile.txt | - And for user "Alice" the space "Personal" should not contain these entries: - | vaultFolder | - | vaultFile.txt | - - @env-config @keycloak-config - Scenario: user can set custom auth level names - Given the administrator has set the Keycloak realm attribute "acr.loa.map" to '{"regular":"1","testing":"2"}' - And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" - And user "Alice" has logged in via web UI - When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API - Then the HTTP status code should be "201" - And user "Alice" should have a JWT token with an ACR value "testing" - - - Scenario: check capabilities endpoint for vault - Given using OCS API version "2" - And user "Alice" has logged in via web UI - When user "Alice" retrieves the vault mode capabilities using the capabilities API - Then the OCS status code should be "200" - And the HTTP status code should be "200" - And the ocs JSON data of the response should match - """ - { - "type": "object", - "required": [ "capabilities" ], - "properties": { - "capabilities": { - "type": "object", - "required": [ - "core", - "files", - "files_sharing", - "auth", - "vault" - ], - "properties": { - "files_sharing": { - "type": "object", - "required": [ - "api_enabled", - "default_permissions", - "public", - "resharing", - "federation", - "group_sharing", - "share_with_group_members_only", - "share_with_membership_groups_only", - "auto_accept_share", - "user_enumeration" - ], - "properties": { - "federation": { - "type": "object", - "required": [ - "outgoing", - "incoming" - ], - "properties": { - "outgoing": { - "const": false - }, - "incoming": { - "const": false - } - } - }, - "public": { - "type": "object", - "required": [ - "enabled", - "multiple", - "upload", - "supports_upload_only", - "send_mail", - "social_share" - ], - "properties": { - "enabled": { - "const": false - } - } - } - } - }, - "auth": { - "type": "object", - "required": [ - "mfa" - ], - "properties": { - "mfa": { - "type": "object", - "required": [ - "enabled", - "levelnames" - ], - "properties": { - "enabled": { - "const": true - }, - "levelnames": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "const": "advanced" - } - } - } - } - } - }, - "vault": { - "type": "object", - "required": [ - "enabled", - "vault_storage_provider" - ], - "properties": { - "enabled": { - "const": true - }, - "vault_storage_provider": { - "pattern": "%uuidv4_pattern%" - } - } - } - } - } - } - } - """ - - - Scenario: user copies folder from drive to vault - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "driveFolder" in space "Personal" - When user "Alice" copies folder "driveFolder" from space "Personal" to "driveFolder" inside space "Personal" in vault using the WebDAV API - Then the HTTP status code should be "201" - And for user "Alice" the space "Personal" in vault should contain these entries: - | driveFolder | - And for user "Alice" the space "Personal" should contain these entries: - | driveFolder | - - - Scenario: user copies file from drive to vault - Given user "Alice" has logged in via web UI - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" - When user "Alice" copies file "testfile.txt" from space "Personal" to "testfile.txt" inside space "Personal" in vault using the WebDAV API - Then the HTTP status code should be "201" - And for user "Alice" the content of the file "testfile.txt" of the space "Personal" in vault should be "some content" - And for user "Alice" the space "Personal" should contain these entries: - | testfile.txt | - - - Scenario: user tries to copy folder from vault to drive - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - When user "Alice" copies folder "vaultFolder" from space "Personal" in vault to "vaultFolder" inside space "Personal" using the WebDAV API - Then the HTTP status code should be "409" - And for user "Alice" the space "Personal" should not contain these entries: - | vaultFolder | - And for user "Alice" the space "Personal" in vault should contain these entries: - | vaultFolder | - - - Scenario: user tries to copy file from vault to drive - Given user "Alice" has logged in via web UI - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" in vault - When user "Alice" copies file "testfile.txt" from space "Personal" in vault to "testfile.txt" inside space "Personal" using the WebDAV API - Then the HTTP status code should be "409" - And for user "Alice" the space "Personal" should not contain these entries: - | testfile.txt | - And for user "Alice" the space "Personal" in vault should contain these entries: - | testfile.txt | - - - Scenario: user copies sub-folder from drive to vault - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "driveFolder" in space "Personal" - And user "Alice" has created a folder "driveFolder/subFolder" in space "Personal" - When user "Alice" copies folder "driveFolder/subFolder" from space "Personal" to "subFolder" inside space "Personal" in vault using the WebDAV API - Then the HTTP status code should be "201" - And for user "Alice" the space "Personal" in vault should contain these entries: - | subFolder | - And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: - | subFolder | - - - Scenario: user copies file inside folder from drive to vault - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "driveFolder" in space "Personal" - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFolder/testfile.txt" - When user "Alice" copies file "driveFolder/testfile.txt" from space "Personal" to "testfile.txt" inside space "Personal" in vault using the WebDAV API - Then the HTTP status code should be "201" - And for user "Alice" the content of the file "testfile.txt" of the space "Personal" in vault should be "some content" - And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: - | testfile.txt | - - - Scenario: user copies sub-folder from drive to a folder in vault - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "driveFolder" in space "Personal" - And user "Alice" has created a folder "driveFolder/subFolder" in space "Personal" - And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - When user "Alice" copies folder "driveFolder/subFolder" from space "Personal" to "vaultFolder/subFolder" inside space "Personal" in vault using the WebDAV API - Then the HTTP status code should be "201" - And for user "Alice" folder "vaultFolder" of the space "Personal" in vault should contain these entries: - | subFolder | - And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: - | subFolder | - - - Scenario: user copies file inside folder from drive to a folder in vault - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "driveFolder" in space "Personal" - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFolder/testfile.txt" - And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - When user "Alice" copies file "driveFolder/testfile.txt" from space "Personal" to "vaultFolder/testfile.txt" inside space "Personal" in vault using the WebDAV API - Then the HTTP status code should be "201" - And for user "Alice" the content of the file "vaultFolder/testfile.txt" of the space "Personal" in vault should be "some content" - And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: - | testfile.txt | - - - Scenario: user tries to create a public link of a folder inside vault - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - When user "Alice" creates the following resource link share using the Graph API: - | resource | vaultFolder | - | space | Personal | - | permissionsRole | View | - | storage | vault | - Then the HTTP status code should be "400" - And the JSON data of the response should match - """ - { - "type": "object", - "required": ["error"], - "properties": { - "error": { - "type": "object", - "required": ["code", "innererror", "message"], - "properties": { - "code": { - "const": "invalidRequest" - }, - "innererror": { - "type": "object", - "required": [ - "date", - "request-id" - ] - }, - "message": { - "const": "public links are not allowed for vault resources" - } - } - } - } - } - """ - - - Scenario: user tries to create a public link of a file inside vault - Given user "Alice" has logged in via web UI - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" in vault - When user "Alice" creates the following resource link share using the Graph API: - | resource | testfile.txt | - | space | Personal | - | permissionsRole | View | - | storage | vault | - Then the HTTP status code should be "400" - And the JSON data of the response should match - """ - { - "type": "object", - "required": ["error"], - "properties": { - "error": { - "type": "object", - "required": ["code", "innererror", "message"], - "properties": { - "code": { - "const": "invalidRequest" - }, - "innererror": { - "type": "object", - "required": [ - "date", - "request-id" - ] - }, - "message": { - "const": "public links are not allowed for vault resources" - } - } - } - } - } - """ - - - Scenario: user tries to create a public link of a space root inside vault - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Alice" has created a space "vault-space" in vault with the default quota using the Graph API - When user "Alice" tries to create the following space link share using permissions endpoint of the Graph API: - | space | vault-space | - | permissionsRole | View | - | storage | vault | - Then the HTTP status code should be "400" - And the JSON data of the response should match - """ - { - "type": "object", - "required": ["error"], - "properties": { - "error": { - "type": "object", - "required": ["code", "innererror", "message"], - "properties": { - "code": { - "const": "invalidRequest" - }, - "innererror": { - "type": "object", - "required": [ - "date", - "request-id" - ] - }, - "message": { - "const": "public links are not allowed for vault resources" - } - } - } - } - } - """ - - - Scenario Outline: send share invitation for project space in vault to user with different roles (permissions endpoint) - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Brian" has been created with default attributes - And user "Brian" has logged in via web UI - And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - | space | new-space | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "200" - And the JSON data of the response should match - """ - { - "type": "object", - "required": [ - "value" - ], - "properties": { - "value": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "grantedToV2", - "roles" - ], - "properties": { - "grantedToV2": { - "type": "object", - "required": [ - "user" - ], - "properties": { - "user": { - "type": "object", - "required": [ - "displayName", - "id" - ], - "properties": { - "displayName": { - "const": "Brian Murphy" - }, - "id": { - "type": "string", - "pattern": "^%user_id_pattern%$" - } - } - } - } - }, - "roles": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "string", - "pattern": "^%role_id_pattern%$" - } - } - } - } - } - } - } - """ - Examples: - | permissions-role | - | Space Viewer | - | Space Editor | - | Manager | - - - Scenario Outline: send share invitation for disabled project space in vault to user with different roles (permissions endpoint) - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Brian" has been created with default attributes - And user "Brian" has logged in via web UI - And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - And user "Admin" has disabled a space "new-space" in vault - When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - | space | new-space | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "404" - And the JSON data of the response should match - """ - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "object", - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "const": "itemNotFound" - }, - "message": { - "type": "string", - "pattern": "^stat: error: not found: %user_id_pattern%$" - } - } - } - } - } - """ - Examples: - | permissions-role | - | Space Viewer | - | Space Editor | - | Manager | - - - Scenario Outline: send share invitation for deleted project space in vault to user with different roles (permissions endpoint) - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Brian" has been created with default attributes - And user "Brian" has logged in via web UI - And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - And user "Admin" has disabled a space "new-space" in vault - And user "Admin" has deleted a space "new-space" in vault - When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - | space | new-space | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "404" - And the JSON data of the response should match - """ - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "object", - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "const": "itemNotFound" - }, - "message": { - "const": "stat: error: not found: " - } - } - } - } - } - """ - Examples: - | permissions-role | - | Space Viewer | - | Space Editor | - | Manager | - - - Scenario Outline: try to send share invitation for personal space in vault to user with different roles (permissions endpoint) - Given user "Alice" has logged in via web UI - And user "Brian" has been created with default attributes - And user "Brian" has logged in via web UI - When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - | space | Personal | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "400" - And the JSON data of the response should match - """ - { - "type": "object", - "required": ["error"], - "properties": { - "error": { - "type": "object", - "required": ["code", "innererror", "message"], - "properties": { - "code": { - "const": "invalidRequest" - }, - "innererror": { - "type": "object", - "required": [ - "date", - "request-id" - ] - }, - "message": { - "const": "space type is not eligible for sharing" - } - } - } - } - } - """ - Examples: - | permissions-role | - | Space Viewer | - | Space Editor | - | Manager | - - - Scenario Outline: try to share Shares space in vault with a user (permissions endpoint) - Given user "Alice" has logged in via web UI - And user "Brian" has been created with default attributes - And user "Brian" has logged in via web UI - When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - | space | Shares | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "400" - And the JSON data of the response should match - """ - { - "type": "object", - "required": ["error"], - "properties": { - "error": { - "type": "object", - "required": ["code", "innererror", "message"], - "properties": { - "code": { - "const": "invalidRequest" - }, - "innererror": { - "type": "object", - "required": [ - "date", - "request-id" - ] - }, - "message": { - "const": "" - } - } - } - } - } - """ - Examples: - | permissions-role | error-message | - | Space Viewer | role not applicable to this resource | - | Space Editor | role not applicable to this resource | - | Manager | role not applicable to this resource | - - - Scenario Outline: invite user to a project space in vault with different roles using root endpoint - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Brian" has been created with default attributes - And user "Brian" has logged in via web UI - And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - When user "Alice" sends the following space share invitation using root endpoint of the Graph API: - | space | new-space | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "200" - And the JSON data of the response should match - """ - { - "type": "object", - "required": [ - "value" - ], - "properties": { - "value": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "object", - "required": [ - "grantedToV2", - "roles" - ], - "properties": { - "grantedToV2": { - "type": "object", - "required": [ - "user" - ], - "properties": { - "user": { - "type": "object", - "required": [ - "displayName", - "id" - ], - "properties": { - "displayName": { - "type": "string", - "const": "Brian Murphy" - }, - "id": { - "type": "string", - "pattern": "^%user_id_pattern%$" - } - } - } - } - }, - "roles": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "items": { - "type": "string", - "pattern": "^%role_id_pattern%$" - } - } - } - } - } - } - } - """ - Examples: - | permissions-role | - | Space Viewer | - | Space Editor | - | Manager | - - - Scenario Outline: try to invite user to personal drive in vault with different roles using root endpoint - Given user "Alice" has logged in via web UI - And user "Brian" has been created with default attributes - And user "Brian" has logged in via web UI - When user "Alice" tries to send the following space share invitation using root endpoint of the Graph API: - | space | Personal | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "400" - And the JSON data of the response should match - """ - { - "type": "object", - "required": ["error"], - "properties": { - "error": { - "type": "object", - "required": ["code", "innererror", "message"], - "properties": { - "code": { - "const": "invalidRequest" - }, - "innererror": { - "type": "object", - "required": [ - "date", - "request-id" - ] - }, - "message": { - "const": "unsupported space type" - } - } - } - } - } - """ - Examples: - | permissions-role | - | Space Viewer | - | Space Editor | - | Manager | - - - Scenario Outline: try to invite user to Shares drive in vault with different roles using root endpoint - Given user "Alice" has logged in via web UI - And user "Brian" has been created with default attributes - And user "Brian" has logged in via web UI - When user "Alice" tries to send the following space share invitation using root endpoint of the Graph API: - | space | Shares | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "400" - And the JSON data of the response should match - """ - { - "type": "object", - "required": ["error"], - "properties": { - "error": { - "type": "object", - "required": ["code", "innererror", "message"], - "properties": { - "code": { - "const": "invalidRequest" - }, - "innererror": { - "type": "object", - "required": [ - "date", - "request-id" - ] - }, - "message": { - "const": "unsupported space type" - } - } - } - } - } - """ - Examples: - | permissions-role | - | Space Viewer | - | Space Editor | - | Manager | - - - Scenario: search results for resources in Personal space should be isolated between vault and drive - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "testDriveFolder" in space "Personal" - And user "Alice" has created a folder "testVaultFolder" in space "Personal" in vault - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testDriveFile.txt" - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testVaultFile.txt" in vault - When user "Alice" searches for "*test*" inside space "Personal" in vault using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "2" entries - And the search result of user "Alice" should contain only these entries: - | testVaultFolder | - | testVaultFile.txt | - When user "Alice" searches for "*test*" inside space "Personal" using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "2" entries - And the search result of user "Alice" should contain only these entries: - | testDriveFolder | - | testDriveFile.txt | - - - Scenario: search results for resources inside folder with same name should be isolated between vault and drive - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "newFolder" in space "Personal" - And user "Alice" has created a folder "newFolder" in space "Personal" in vault - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "newFolder/testDriveFile.txt" - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "newFolder/testVaultFile.txt" in vault - When user "Alice" searches for "*test*" inside folder "newFolder" in space "Personal" in vault using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "1" entries - And the search result of user "Alice" should contain only these entries: - | newFolder/testVaultFile.txt | - When user "Alice" searches for "*test*" inside folder "newFolder" in space "Personal" using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "1" entries - And the search result of user "Alice" should contain only these entries: - | newFolder/testDriveFile.txt | - - - Scenario: search result for resources inside project spaces with same name should be isolated between vault and drive - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Alice" has created a space "new-space" with the default quota using the Graph API - And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - And user "Alice" has created a folder "testDriveFolder" in space "new-space" - And user "Alice" has created a folder "testVaultFolder" in space "new-space" in vault - And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testDriveFile.txt" - And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testVaultFile.txt" in vault - When user "Alice" searches for "*test*" inside space "new-space" in vault using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "2" entries - And the search result of user "Alice" should contain only these entries: - | testVaultFolder | - | testVaultFile.txt | - When user "Alice" searches for "*test*" inside space "new-space" using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "2" entries - And the search result of user "Alice" should contain only these entries: - | testDriveFolder | - | testDriveFile.txt | - - @tikaServiceNeeded - Scenario: search result by content of file should be isolated between vault and drive - Given user "Alice" has logged in via web UI - And user "Alice" has uploaded a file inside space "Personal" with content "content of file in drive" to "testDriveFile.txt" - And user "Alice" has uploaded a file inside space "Personal" with content "content of file in vault" to "testVaultFile.txt" in vault - When user "Alice" searches for "Content:content" in vault using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "1" entries - And the search result of user "Alice" should contain only these files: - | testVaultFile.txt | - When user "Alice" searches for "Content:content" using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "1" entries - And the search result of user "Alice" should contain only these files: - | testDriveFile.txt | - - @tikaServiceNeeded - Scenario: search result by content of file inside project space should be isolated between vault and drive - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Alice" has created a space "new-space" with the default quota using the Graph API - And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - And user "Alice" has uploaded a file inside space "new-space" with content "content of file in drive" to "testDriveFile.txt" - And user "Alice" has uploaded a file inside space "new-space" with content "content of file in vault" to "testVaultFile.txt" in vault - When user "Alice" searches for "Content:content" in vault using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "1" entries - And the search result of user "Alice" should contain only these files: - | testVaultFile.txt | - When user "Alice" searches for "Content:content" using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "1" entries - And the search result of user "Alice" should contain only these files: - | testDriveFile.txt | - - - Scenario: search results by resource tags should be isolated between vault and drive - Given user "Alice" has logged in via web UI - And user "Alice" has created a folder "driveFolder" in space "Personal" - And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testDriveFile.txt" - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testVaultFile.txt" in vault - And user "Alice" has tagged the following files of the space "Personal": - | path | tagName | - | testDriveFile.txt | tag1 | - And user "Alice" has tagged the following folders of the space "Personal": - | path | tagName | - | driveFolder | tag1 | - And user "Alice" has tagged the following files of the space "Personal" in vault: - | path | tagName | - | testVaultFile.txt | tag1 | - And user "Alice" has tagged the following folders of the space "Personal" in vault: - | path | tagName | - | vaultFolder | tag1 | - When user "Alice" searches for "Tags:tag1" in vault using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "2" entries - And the search result of user "Alice" should contain only these files: - | testVaultFile.txt | - | vaultFolder | - When user "Alice" searches for "Tags:tag1" using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "2" entries - And the search result of user "Alice" should contain only these files: - | testDriveFile.txt | - | driveFolder | - - - Scenario: search results by resource tags inside project space should be isolated between vault and drive - Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Alice" has created a space "new-space" with the default quota using the Graph API - And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - And user "Alice" has created a folder "driveFolder" in space "new-space" - And user "Alice" has created a folder "vaultFolder" in space "new-space" in vault - And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testDriveFile.txt" - And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testVaultFile.txt" in vault - And user "Alice" has tagged the following files of the space "new-space": - | path | tagName | - | testDriveFile.txt | tag1 | - And user "Alice" has tagged the following folders of the space "new-space": - | path | tagName | - | driveFolder | tag1 | - And user "Alice" has tagged the following files of the space "new-space" in vault: - | path | tagName | - | testVaultFile.txt | tag1 | - And user "Alice" has tagged the following folders of the space "new-space" in vault: - | path | tagName | - | vaultFolder | tag1 | - When user "Alice" searches for "Tags:tag1" in vault using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "2" entries - And the search result of user "Alice" should contain only these files: - | testVaultFile.txt | - | vaultFolder | - When user "Alice" searches for "Tags:tag1" using the WebDAV API - Then the HTTP status code should be "207" - And the search result should contain "2" entries - And the search result of user "Alice" should contain only these files: - | testDriveFile.txt | - | driveFolder | - - - Scenario Outline: folder share received from vault and drive personal space should be isolated - Given user "Brian" has been created with default attributes - And user "Alice" has logged in via web UI - And user "Brian" has logged in via web UI - And user "Alice" has created a folder "driveFolder" in space "Personal" - And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - And user "Alice" has sent the following resource share invitation: - | resource | driveFolder | - | space | Personal | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - When user "Alice" sends the following resource share invitation using the Graph API: - | resource | vaultFolder | - | space | Personal | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "200" - And user "Brian" should have a share in vault "vaultFolder" synced - And user "Brian" should have the following resource shares: - | resource | permissionsRole | sharer | space | storage | - | vaultFolder | | Alice | Personal | vault | - And user "Brian" should have the following resource shares: - | resource | permissionsRole | sharer | space | - | driveFolder | | Alice | Personal | - Examples: - | permissions-role | - | Viewer | - | Editor | - | Uploader | - - - Scenario Outline: file share received from vault and drive personal space should be isolated - Given user "Brian" has been created with default attributes - And user "Alice" has logged in via web UI - And user "Brian" has logged in via web UI - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFile.txt" - And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault - And user "Alice" has sent the following resource share invitation: - | resource | driveFile.txt | - | space | Personal | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - When user "Alice" sends the following resource share invitation using the Graph API: - | resource | vaultFile.txt | - | space | Personal | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "200" - And user "Brian" should have a share in vault "vaultFile.txt" synced - And user "Brian" should have the following resource shares: - | resource | permissionsRole | sharer | space | storage | - | vaultFile.txt | | Alice | Personal | vault | - And user "Brian" should have the following resource shares: - | resource | permissionsRole | sharer | space | - | driveFile.txt | | Alice | Personal | - Examples: - | permissions-role | - | Viewer | - | File Editor | - - - Scenario Outline: folder share received from vault and drive project space should be isolated - Given user "Brian" has been created with default attributes - And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - And user "Alice" has logged in via web UI - And user "Brian" has logged in via web UI - And user "Alice" has created a space "new-space" with the default quota using the Graph API - And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - And user "Alice" has created a folder "driveFolder" in space "new-space" - And user "Alice" has created a folder "vaultFolder" in space "new-space" in vault - And user "Alice" has sent the following resource share invitation: - | resource | driveFolder | - | space | new-space | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - When user "Alice" sends the following resource share invitation using the Graph API: - | resource | vaultFolder | - | space | new-space | - | sharee | Brian | - | shareType | user | - | permissionsRole | | - | storage | vault | - Then the HTTP status code should be "200" - And user "Brian" should have a share in vault "vaultFolder" synced - And user "Brian" should have the following resource shares: - | resource | permissionsRole | sharer | space | storage | - | vaultFolder | | Alice | new-space | vault | - And user "Brian" should have the following resource shares: - | resource | permissionsRole | sharer | space | - | driveFolder | | Alice | new-space | - Examples: - | permissions-role | - | Viewer | - | Editor | - | Uploader | + # Scenario: user can create folders and files in project space in vault + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Alice" has created a space "vault-space" in vault with the default quota using the Graph API + # When user "Alice" creates a folder "vaultFolder" in space "vault-space" in vault using the WebDav Api + # Then the HTTP status code should be "201" + # When user "Alice" uploads a file inside space "vault-space" with content "some content" to "vaultFile.txt" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And for user "Alice" the space "vault-space" in vault should contain these entries: + # | vaultFolder | + # | vaultFile.txt | + + + # Scenario: resources in drive and vault are isolated + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "driveFolder" in space "Personal" + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFile.txt" + # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + # When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And for user "Alice" the space "Personal" in vault should contain these entries: + # | vaultFolder | + # | vaultFile.txt | + # And for user "Alice" the space "Personal" should contain these entries: + # | driveFolder | + # | driveFile.txt | + # And for user "Alice" the space "Personal" in vault should not contain these entries: + # | driveFolder | + # | driveFile.txt | + # And for user "Alice" the space "Personal" should not contain these entries: + # | vaultFolder | + # | vaultFile.txt | + + # @env-config @keycloak-config + # Scenario: user can set custom auth level names + # Given the administrator has set the Keycloak realm attribute "acr.loa.map" to '{"regular":"1","testing":"2"}' + # And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" + # And user "Alice" has logged in via web UI + # When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And user "Alice" should have a JWT token with an ACR value "testing" + + + # Scenario: check capabilities endpoint for vault + # Given using OCS API version "2" + # And user "Alice" has logged in via web UI + # When user "Alice" retrieves the vault mode capabilities using the capabilities API + # Then the OCS status code should be "200" + # And the HTTP status code should be "200" + # And the ocs JSON data of the response should match + # """ + # { + # "type": "object", + # "required": [ "capabilities" ], + # "properties": { + # "capabilities": { + # "type": "object", + # "required": [ + # "core", + # "files", + # "files_sharing", + # "auth", + # "vault" + # ], + # "properties": { + # "files_sharing": { + # "type": "object", + # "required": [ + # "api_enabled", + # "default_permissions", + # "public", + # "resharing", + # "federation", + # "group_sharing", + # "share_with_group_members_only", + # "share_with_membership_groups_only", + # "auto_accept_share", + # "user_enumeration" + # ], + # "properties": { + # "federation": { + # "type": "object", + # "required": [ + # "outgoing", + # "incoming" + # ], + # "properties": { + # "outgoing": { + # "const": false + # }, + # "incoming": { + # "const": false + # } + # } + # }, + # "public": { + # "type": "object", + # "required": [ + # "enabled", + # "multiple", + # "upload", + # "supports_upload_only", + # "send_mail", + # "social_share" + # ], + # "properties": { + # "enabled": { + # "const": false + # } + # } + # } + # } + # }, + # "auth": { + # "type": "object", + # "required": [ + # "mfa" + # ], + # "properties": { + # "mfa": { + # "type": "object", + # "required": [ + # "enabled", + # "levelnames" + # ], + # "properties": { + # "enabled": { + # "const": true + # }, + # "levelnames": { + # "type": "array", + # "minItems": 1, + # "maxItems": 1, + # "items": { + # "const": "advanced" + # } + # } + # } + # } + # } + # }, + # "vault": { + # "type": "object", + # "required": [ + # "enabled", + # "vault_storage_provider" + # ], + # "properties": { + # "enabled": { + # "const": true + # }, + # "vault_storage_provider": { + # "pattern": "%uuidv4_pattern%" + # } + # } + # } + # } + # } + # } + # } + # """ + + + # Scenario: user copies folder from drive to vault + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "driveFolder" in space "Personal" + # When user "Alice" copies folder "driveFolder" from space "Personal" to "driveFolder" inside space "Personal" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And for user "Alice" the space "Personal" in vault should contain these entries: + # | driveFolder | + # And for user "Alice" the space "Personal" should contain these entries: + # | driveFolder | + + + # Scenario: user copies file from drive to vault + # Given user "Alice" has logged in via web UI + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" + # When user "Alice" copies file "testfile.txt" from space "Personal" to "testfile.txt" inside space "Personal" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And for user "Alice" the content of the file "testfile.txt" of the space "Personal" in vault should be "some content" + # And for user "Alice" the space "Personal" should contain these entries: + # | testfile.txt | + + + # Scenario: user tries to copy folder from vault to drive + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + # When user "Alice" copies folder "vaultFolder" from space "Personal" in vault to "vaultFolder" inside space "Personal" using the WebDAV API + # Then the HTTP status code should be "409" + # And for user "Alice" the space "Personal" should not contain these entries: + # | vaultFolder | + # And for user "Alice" the space "Personal" in vault should contain these entries: + # | vaultFolder | + + + # Scenario: user tries to copy file from vault to drive + # Given user "Alice" has logged in via web UI + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" in vault + # When user "Alice" copies file "testfile.txt" from space "Personal" in vault to "testfile.txt" inside space "Personal" using the WebDAV API + # Then the HTTP status code should be "409" + # And for user "Alice" the space "Personal" should not contain these entries: + # | testfile.txt | + # And for user "Alice" the space "Personal" in vault should contain these entries: + # | testfile.txt | + + + # Scenario: user copies sub-folder from drive to vault + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "driveFolder" in space "Personal" + # And user "Alice" has created a folder "driveFolder/subFolder" in space "Personal" + # When user "Alice" copies folder "driveFolder/subFolder" from space "Personal" to "subFolder" inside space "Personal" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And for user "Alice" the space "Personal" in vault should contain these entries: + # | subFolder | + # And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: + # | subFolder | + + + # Scenario: user copies file inside folder from drive to vault + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "driveFolder" in space "Personal" + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFolder/testfile.txt" + # When user "Alice" copies file "driveFolder/testfile.txt" from space "Personal" to "testfile.txt" inside space "Personal" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And for user "Alice" the content of the file "testfile.txt" of the space "Personal" in vault should be "some content" + # And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: + # | testfile.txt | + + + # Scenario: user copies sub-folder from drive to a folder in vault + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "driveFolder" in space "Personal" + # And user "Alice" has created a folder "driveFolder/subFolder" in space "Personal" + # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + # When user "Alice" copies folder "driveFolder/subFolder" from space "Personal" to "vaultFolder/subFolder" inside space "Personal" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And for user "Alice" folder "vaultFolder" of the space "Personal" in vault should contain these entries: + # | subFolder | + # And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: + # | subFolder | + + + # Scenario: user copies file inside folder from drive to a folder in vault + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "driveFolder" in space "Personal" + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFolder/testfile.txt" + # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + # When user "Alice" copies file "driveFolder/testfile.txt" from space "Personal" to "vaultFolder/testfile.txt" inside space "Personal" in vault using the WebDAV API + # Then the HTTP status code should be "201" + # And for user "Alice" the content of the file "vaultFolder/testfile.txt" of the space "Personal" in vault should be "some content" + # And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: + # | testfile.txt | + + + # Scenario: user tries to create a public link of a folder inside vault + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + # When user "Alice" creates the following resource link share using the Graph API: + # | resource | vaultFolder | + # | space | Personal | + # | permissionsRole | View | + # | storage | vault | + # Then the HTTP status code should be "400" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": ["error"], + # "properties": { + # "error": { + # "type": "object", + # "required": ["code", "innererror", "message"], + # "properties": { + # "code": { + # "const": "invalidRequest" + # }, + # "innererror": { + # "type": "object", + # "required": [ + # "date", + # "request-id" + # ] + # }, + # "message": { + # "const": "public links are not allowed for vault resources" + # } + # } + # } + # } + # } + # """ + + + # Scenario: user tries to create a public link of a file inside vault + # Given user "Alice" has logged in via web UI + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" in vault + # When user "Alice" creates the following resource link share using the Graph API: + # | resource | testfile.txt | + # | space | Personal | + # | permissionsRole | View | + # | storage | vault | + # Then the HTTP status code should be "400" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": ["error"], + # "properties": { + # "error": { + # "type": "object", + # "required": ["code", "innererror", "message"], + # "properties": { + # "code": { + # "const": "invalidRequest" + # }, + # "innererror": { + # "type": "object", + # "required": [ + # "date", + # "request-id" + # ] + # }, + # "message": { + # "const": "public links are not allowed for vault resources" + # } + # } + # } + # } + # } + # """ + + + # Scenario: user tries to create a public link of a space root inside vault + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Alice" has created a space "vault-space" in vault with the default quota using the Graph API + # When user "Alice" tries to create the following space link share using permissions endpoint of the Graph API: + # | space | vault-space | + # | permissionsRole | View | + # | storage | vault | + # Then the HTTP status code should be "400" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": ["error"], + # "properties": { + # "error": { + # "type": "object", + # "required": ["code", "innererror", "message"], + # "properties": { + # "code": { + # "const": "invalidRequest" + # }, + # "innererror": { + # "type": "object", + # "required": [ + # "date", + # "request-id" + # ] + # }, + # "message": { + # "const": "public links are not allowed for vault resources" + # } + # } + # } + # } + # } + # """ + + + # Scenario Outline: send share invitation for project space in vault to user with different roles (permissions endpoint) + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Brian" has been created with default attributes + # And user "Brian" has logged in via web UI + # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + # | space | new-space | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "200" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": [ + # "value" + # ], + # "properties": { + # "value": { + # "type": "array", + # "minItems": 1, + # "maxItems": 1, + # "items": { + # "type": "object", + # "required": [ + # "grantedToV2", + # "roles" + # ], + # "properties": { + # "grantedToV2": { + # "type": "object", + # "required": [ + # "user" + # ], + # "properties": { + # "user": { + # "type": "object", + # "required": [ + # "displayName", + # "id" + # ], + # "properties": { + # "displayName": { + # "const": "Brian Murphy" + # }, + # "id": { + # "type": "string", + # "pattern": "^%user_id_pattern%$" + # } + # } + # } + # } + # }, + # "roles": { + # "type": "array", + # "minItems": 1, + # "maxItems": 1, + # "items": { + # "type": "string", + # "pattern": "^%role_id_pattern%$" + # } + # } + # } + # } + # } + # } + # } + # """ + # Examples: + # | permissions-role | + # | Space Viewer | + # | Space Editor | + # | Manager | + + + # Scenario Outline: send share invitation for disabled project space in vault to user with different roles (permissions endpoint) + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Brian" has been created with default attributes + # And user "Brian" has logged in via web UI + # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + # And user "Admin" has disabled a space "new-space" in vault + # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + # | space | new-space | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "404" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": [ + # "error" + # ], + # "properties": { + # "error": { + # "type": "object", + # "required": [ + # "code", + # "message" + # ], + # "properties": { + # "code": { + # "const": "itemNotFound" + # }, + # "message": { + # "type": "string", + # "pattern": "^stat: error: not found: %user_id_pattern%$" + # } + # } + # } + # } + # } + # """ + # Examples: + # | permissions-role | + # | Space Viewer | + # | Space Editor | + # | Manager | + + + # Scenario Outline: send share invitation for deleted project space in vault to user with different roles (permissions endpoint) + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Brian" has been created with default attributes + # And user "Brian" has logged in via web UI + # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + # And user "Admin" has disabled a space "new-space" in vault + # And user "Admin" has deleted a space "new-space" in vault + # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + # | space | new-space | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "404" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": [ + # "error" + # ], + # "properties": { + # "error": { + # "type": "object", + # "required": [ + # "code", + # "message" + # ], + # "properties": { + # "code": { + # "const": "itemNotFound" + # }, + # "message": { + # "const": "stat: error: not found: " + # } + # } + # } + # } + # } + # """ + # Examples: + # | permissions-role | + # | Space Viewer | + # | Space Editor | + # | Manager | + + + # Scenario Outline: try to send share invitation for personal space in vault to user with different roles (permissions endpoint) + # Given user "Alice" has logged in via web UI + # And user "Brian" has been created with default attributes + # And user "Brian" has logged in via web UI + # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + # | space | Personal | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "400" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": ["error"], + # "properties": { + # "error": { + # "type": "object", + # "required": ["code", "innererror", "message"], + # "properties": { + # "code": { + # "const": "invalidRequest" + # }, + # "innererror": { + # "type": "object", + # "required": [ + # "date", + # "request-id" + # ] + # }, + # "message": { + # "const": "space type is not eligible for sharing" + # } + # } + # } + # } + # } + # """ + # Examples: + # | permissions-role | + # | Space Viewer | + # | Space Editor | + # | Manager | + + + # Scenario Outline: try to share Shares space in vault with a user (permissions endpoint) + # Given user "Alice" has logged in via web UI + # And user "Brian" has been created with default attributes + # And user "Brian" has logged in via web UI + # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + # | space | Shares | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "400" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": ["error"], + # "properties": { + # "error": { + # "type": "object", + # "required": ["code", "innererror", "message"], + # "properties": { + # "code": { + # "const": "invalidRequest" + # }, + # "innererror": { + # "type": "object", + # "required": [ + # "date", + # "request-id" + # ] + # }, + # "message": { + # "const": "" + # } + # } + # } + # } + # } + # """ + # Examples: + # | permissions-role | error-message | + # | Space Viewer | role not applicable to this resource | + # | Space Editor | role not applicable to this resource | + # | Manager | role not applicable to this resource | + + + # Scenario Outline: invite user to a project space in vault with different roles using root endpoint + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Brian" has been created with default attributes + # And user "Brian" has logged in via web UI + # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + # When user "Alice" sends the following space share invitation using root endpoint of the Graph API: + # | space | new-space | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "200" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": [ + # "value" + # ], + # "properties": { + # "value": { + # "type": "array", + # "minItems": 1, + # "maxItems": 1, + # "items": { + # "type": "object", + # "required": [ + # "grantedToV2", + # "roles" + # ], + # "properties": { + # "grantedToV2": { + # "type": "object", + # "required": [ + # "user" + # ], + # "properties": { + # "user": { + # "type": "object", + # "required": [ + # "displayName", + # "id" + # ], + # "properties": { + # "displayName": { + # "type": "string", + # "const": "Brian Murphy" + # }, + # "id": { + # "type": "string", + # "pattern": "^%user_id_pattern%$" + # } + # } + # } + # } + # }, + # "roles": { + # "type": "array", + # "minItems": 1, + # "maxItems": 1, + # "items": { + # "type": "string", + # "pattern": "^%role_id_pattern%$" + # } + # } + # } + # } + # } + # } + # } + # """ + # Examples: + # | permissions-role | + # | Space Viewer | + # | Space Editor | + # | Manager | + + + # Scenario Outline: try to invite user to personal drive in vault with different roles using root endpoint + # Given user "Alice" has logged in via web UI + # And user "Brian" has been created with default attributes + # And user "Brian" has logged in via web UI + # When user "Alice" tries to send the following space share invitation using root endpoint of the Graph API: + # | space | Personal | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "400" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": ["error"], + # "properties": { + # "error": { + # "type": "object", + # "required": ["code", "innererror", "message"], + # "properties": { + # "code": { + # "const": "invalidRequest" + # }, + # "innererror": { + # "type": "object", + # "required": [ + # "date", + # "request-id" + # ] + # }, + # "message": { + # "const": "unsupported space type" + # } + # } + # } + # } + # } + # """ + # Examples: + # | permissions-role | + # | Space Viewer | + # | Space Editor | + # | Manager | + + + # Scenario Outline: try to invite user to Shares drive in vault with different roles using root endpoint + # Given user "Alice" has logged in via web UI + # And user "Brian" has been created with default attributes + # And user "Brian" has logged in via web UI + # When user "Alice" tries to send the following space share invitation using root endpoint of the Graph API: + # | space | Shares | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "400" + # And the JSON data of the response should match + # """ + # { + # "type": "object", + # "required": ["error"], + # "properties": { + # "error": { + # "type": "object", + # "required": ["code", "innererror", "message"], + # "properties": { + # "code": { + # "const": "invalidRequest" + # }, + # "innererror": { + # "type": "object", + # "required": [ + # "date", + # "request-id" + # ] + # }, + # "message": { + # "const": "unsupported space type" + # } + # } + # } + # } + # } + # """ + # Examples: + # | permissions-role | + # | Space Viewer | + # | Space Editor | + # | Manager | + + + # Scenario: search results for resources in Personal space should be isolated between vault and drive + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "testDriveFolder" in space "Personal" + # And user "Alice" has created a folder "testVaultFolder" in space "Personal" in vault + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testDriveFile.txt" + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testVaultFile.txt" in vault + # When user "Alice" searches for "*test*" inside space "Personal" in vault using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "2" entries + # And the search result of user "Alice" should contain only these entries: + # | testVaultFolder | + # | testVaultFile.txt | + # When user "Alice" searches for "*test*" inside space "Personal" using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "2" entries + # And the search result of user "Alice" should contain only these entries: + # | testDriveFolder | + # | testDriveFile.txt | + + + # Scenario: search results for resources inside folder with same name should be isolated between vault and drive + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "newFolder" in space "Personal" + # And user "Alice" has created a folder "newFolder" in space "Personal" in vault + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "newFolder/testDriveFile.txt" + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "newFolder/testVaultFile.txt" in vault + # When user "Alice" searches for "*test*" inside folder "newFolder" in space "Personal" in vault using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "1" entries + # And the search result of user "Alice" should contain only these entries: + # | newFolder/testVaultFile.txt | + # When user "Alice" searches for "*test*" inside folder "newFolder" in space "Personal" using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "1" entries + # And the search result of user "Alice" should contain only these entries: + # | newFolder/testDriveFile.txt | + + + # Scenario: search result for resources inside project spaces with same name should be isolated between vault and drive + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Alice" has created a space "new-space" with the default quota using the Graph API + # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + # And user "Alice" has created a folder "testDriveFolder" in space "new-space" + # And user "Alice" has created a folder "testVaultFolder" in space "new-space" in vault + # And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testDriveFile.txt" + # And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testVaultFile.txt" in vault + # When user "Alice" searches for "*test*" inside space "new-space" in vault using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "2" entries + # And the search result of user "Alice" should contain only these entries: + # | testVaultFolder | + # | testVaultFile.txt | + # When user "Alice" searches for "*test*" inside space "new-space" using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "2" entries + # And the search result of user "Alice" should contain only these entries: + # | testDriveFolder | + # | testDriveFile.txt | + + # @tikaServiceNeeded + # Scenario: search result by content of file should be isolated between vault and drive + # Given user "Alice" has logged in via web UI + # And user "Alice" has uploaded a file inside space "Personal" with content "content of file in drive" to "testDriveFile.txt" + # And user "Alice" has uploaded a file inside space "Personal" with content "content of file in vault" to "testVaultFile.txt" in vault + # When user "Alice" searches for "Content:content" in vault using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "1" entries + # And the search result of user "Alice" should contain only these files: + # | testVaultFile.txt | + # When user "Alice" searches for "Content:content" using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "1" entries + # And the search result of user "Alice" should contain only these files: + # | testDriveFile.txt | + + # @tikaServiceNeeded + # Scenario: search result by content of file inside project space should be isolated between vault and drive + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Alice" has created a space "new-space" with the default quota using the Graph API + # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + # And user "Alice" has uploaded a file inside space "new-space" with content "content of file in drive" to "testDriveFile.txt" + # And user "Alice" has uploaded a file inside space "new-space" with content "content of file in vault" to "testVaultFile.txt" in vault + # When user "Alice" searches for "Content:content" in vault using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "1" entries + # And the search result of user "Alice" should contain only these files: + # | testVaultFile.txt | + # When user "Alice" searches for "Content:content" using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "1" entries + # And the search result of user "Alice" should contain only these files: + # | testDriveFile.txt | + + + # Scenario: search results by resource tags should be isolated between vault and drive + # Given user "Alice" has logged in via web UI + # And user "Alice" has created a folder "driveFolder" in space "Personal" + # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testDriveFile.txt" + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testVaultFile.txt" in vault + # And user "Alice" has tagged the following files of the space "Personal": + # | path | tagName | + # | testDriveFile.txt | tag1 | + # And user "Alice" has tagged the following folders of the space "Personal": + # | path | tagName | + # | driveFolder | tag1 | + # And user "Alice" has tagged the following files of the space "Personal" in vault: + # | path | tagName | + # | testVaultFile.txt | tag1 | + # And user "Alice" has tagged the following folders of the space "Personal" in vault: + # | path | tagName | + # | vaultFolder | tag1 | + # When user "Alice" searches for "Tags:tag1" in vault using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "2" entries + # And the search result of user "Alice" should contain only these files: + # | testVaultFile.txt | + # | vaultFolder | + # When user "Alice" searches for "Tags:tag1" using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "2" entries + # And the search result of user "Alice" should contain only these files: + # | testDriveFile.txt | + # | driveFolder | + + + # Scenario: search results by resource tags inside project space should be isolated between vault and drive + # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Alice" has created a space "new-space" with the default quota using the Graph API + # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + # And user "Alice" has created a folder "driveFolder" in space "new-space" + # And user "Alice" has created a folder "vaultFolder" in space "new-space" in vault + # And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testDriveFile.txt" + # And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testVaultFile.txt" in vault + # And user "Alice" has tagged the following files of the space "new-space": + # | path | tagName | + # | testDriveFile.txt | tag1 | + # And user "Alice" has tagged the following folders of the space "new-space": + # | path | tagName | + # | driveFolder | tag1 | + # And user "Alice" has tagged the following files of the space "new-space" in vault: + # | path | tagName | + # | testVaultFile.txt | tag1 | + # And user "Alice" has tagged the following folders of the space "new-space" in vault: + # | path | tagName | + # | vaultFolder | tag1 | + # When user "Alice" searches for "Tags:tag1" in vault using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "2" entries + # And the search result of user "Alice" should contain only these files: + # | testVaultFile.txt | + # | vaultFolder | + # When user "Alice" searches for "Tags:tag1" using the WebDAV API + # Then the HTTP status code should be "207" + # And the search result should contain "2" entries + # And the search result of user "Alice" should contain only these files: + # | testDriveFile.txt | + # | driveFolder | + + + # Scenario Outline: folder share received from vault and drive personal space should be isolated + # Given user "Brian" has been created with default attributes + # And user "Alice" has logged in via web UI + # And user "Brian" has logged in via web UI + # And user "Alice" has created a folder "driveFolder" in space "Personal" + # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + # And user "Alice" has sent the following resource share invitation: + # | resource | driveFolder | + # | space | Personal | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # When user "Alice" sends the following resource share invitation using the Graph API: + # | resource | vaultFolder | + # | space | Personal | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "200" + # And user "Brian" should have a share in vault "vaultFolder" synced + # And user "Brian" should have the following resource shares: + # | resource | permissionsRole | sharer | space | storage | + # | vaultFolder | | Alice | Personal | vault | + # And user "Brian" should have the following resource shares: + # | resource | permissionsRole | sharer | space | + # | driveFolder | | Alice | Personal | + # Examples: + # | permissions-role | + # | Viewer | + # | Editor | + # | Uploader | + + + # Scenario Outline: file share received from vault and drive personal space should be isolated + # Given user "Brian" has been created with default attributes + # And user "Alice" has logged in via web UI + # And user "Brian" has logged in via web UI + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFile.txt" + # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault + # And user "Alice" has sent the following resource share invitation: + # | resource | driveFile.txt | + # | space | Personal | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # When user "Alice" sends the following resource share invitation using the Graph API: + # | resource | vaultFile.txt | + # | space | Personal | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "200" + # And user "Brian" should have a share in vault "vaultFile.txt" synced + # And user "Brian" should have the following resource shares: + # | resource | permissionsRole | sharer | space | storage | + # | vaultFile.txt | | Alice | Personal | vault | + # And user "Brian" should have the following resource shares: + # | resource | permissionsRole | sharer | space | + # | driveFile.txt | | Alice | Personal | + # Examples: + # | permissions-role | + # | Viewer | + # | File Editor | + + + # Scenario Outline: folder share received from vault and drive project space should be isolated + # Given user "Brian" has been created with default attributes + # And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + # And user "Alice" has logged in via web UI + # And user "Brian" has logged in via web UI + # And user "Alice" has created a space "new-space" with the default quota using the Graph API + # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + # And user "Alice" has created a folder "driveFolder" in space "new-space" + # And user "Alice" has created a folder "vaultFolder" in space "new-space" in vault + # And user "Alice" has sent the following resource share invitation: + # | resource | driveFolder | + # | space | new-space | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # When user "Alice" sends the following resource share invitation using the Graph API: + # | resource | vaultFolder | + # | space | new-space | + # | sharee | Brian | + # | shareType | user | + # | permissionsRole | | + # | storage | vault | + # Then the HTTP status code should be "200" + # And user "Brian" should have a share in vault "vaultFolder" synced + # And user "Brian" should have the following resource shares: + # | resource | permissionsRole | sharer | space | storage | + # | vaultFolder | | Alice | new-space | vault | + # And user "Brian" should have the following resource shares: + # | resource | permissionsRole | sharer | space | + # | driveFolder | | Alice | new-space | + # Examples: + # | permissions-role | + # | Viewer | + # | Editor | + # | Uploader | Scenario Outline: folder share received from vault and drive project space should be isolated From 4873de38e92879ce0c1369478bd05950313c63da Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Wed, 2 Sep 2026 12:43:47 +0545 Subject: [PATCH 04/16] test: add post test debug Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 25 +++++ tests/acceptance/TestHelpers/GraphHelper.php | 7 +- tests/acceptance/bootstrap/Provisioning.php | 14 +-- .../features/apiVault/vault.feature | 94 +++++++++---------- tests/config/k8s/setup.sh | 3 + tests/config/k8s/templates/extra.tpl | 22 +++++ tests/config/k8s/values.yaml | 4 +- 7 files changed, 101 insertions(+), 68 deletions(-) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index c254771d88c..c81da35adec 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -387,6 +387,31 @@ jobs: KEYCLOAK: ${{ matrix.suite == 'apiVault' }} KC_URL: https://keycloak:8443 run: make test-acceptance-api + + - name: Debug storage-users-vault (post-test) + if: always() && matrix.suite == 'apiVault' + run: | + echo "=== deployment/pods (label app=storageusers-vault) ===" + kubectl get deployment,pods -n ocis-server -l app=storageusers-vault -o wide || true + + echo "--- proxy: CreateHome-for-vault-personal outcomes (create_home.go), full/untruncated ---" + grep -iE "CreateHome for vault personal|vault personal space creation failed|vault personal space (created|already exists)" tests/config/k8s/logs/*.log 2>/dev/null || true + + echo "--- graph: 'could not create drive' errors (drives.go CreateDrive handler) - did creating the project space itself actually fail server-side despite a 201? ---" + grep -iE "could not create drive" tests/config/k8s/logs/*.log 2>/dev/null || true + + echo "--- storage-users-vault: CreateStorageSpace / CreateContainer rgrpc calls with their result code - is the project space create + the later folder create both landing on this instance, and do they succeed? ---" + grep -iE '"service":"storage-users-vault".*(CreateStorageSpace|CreateContainer)' tests/config/k8s/logs/*.log 2>/dev/null || true + + echo "--- webdav/ocdav: FAILED_PRECONDITION / intermediate collection does not exist - the actual source of the 409 on folder creation (mkcol.go), distinct from AlreadyExists which maps to 405 ---" + grep -iE "FAILED_PRECONDITION|intermediate collection does not exist" tests/config/k8s/logs/*.log 2>/dev/null || true + + echo "--- storage-users-vault: actual on-disk layout at end of run ---" + VAULT_POD=$(kubectl -n ocis-server get pods -l app=storageusers-vault -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [[ -n "$VAULT_POD" ]]; then + echo "pod: $VAULT_POD" + kubectl -n ocis-server exec "$VAULT_POD" -- find /var/lib/ocis -maxdepth 8 2>&1 || true + fi - name: Run Core ${{ matrix.suite }} tests if: startsWith( matrix.suite, 'core' ) diff --git a/tests/acceptance/TestHelpers/GraphHelper.php b/tests/acceptance/TestHelpers/GraphHelper.php index 0715db0c639..4c479186177 100644 --- a/tests/acceptance/TestHelpers/GraphHelper.php +++ b/tests/acceptance/TestHelpers/GraphHelper.php @@ -1004,13 +1004,8 @@ public static function getMySpaces( ): ResponseInterface { $urlArguments = $urlArguments ? "?$urlArguments" : ""; $url = self::getFullUrl($baseUrl, "me/drives" . $urlArguments, $isVault); - var_dump($url); - $response = HttpRequestHelper::get($url, $user, $password, $headers, $body); - var_dump($headers); - var_dump($response->getStatusCode()); - var_dump($response->getBody()->getContents()); - return $response; + return HttpRequestHelper::get($url, $user, $password, $headers, $body); } /** diff --git a/tests/acceptance/bootstrap/Provisioning.php b/tests/acceptance/bootstrap/Provisioning.php index f218cfaeaa9..4e2a67c045c 100644 --- a/tests/acceptance/bootstrap/Provisioning.php +++ b/tests/acceptance/bootstrap/Provisioning.php @@ -96,19 +96,7 @@ public function extractOidcTokenDataFromStorageState(array $state): mixed { $localStorage = $state['origins'][0]['localStorage'] ?? []; foreach ($localStorage as $entry) { if (\str_starts_with($entry['name'] ?? '', 'oc_oAuth.user:')) { - $tokenData = \json_decode($entry['value']); - // TEMPORARY DEBUG: dump the access token's claims (acr in particular) to - // check whether the vault/MFA login flow actually elevates the session. - $accessToken = $tokenData->access_token ?? null; - if (\is_string($accessToken)) { - $segments = \explode('.', $accessToken); - if (\count($segments) === 3) { - $payload = \strtr($segments[1], '-_', '+/'); - $payload .= \str_repeat('=', (4 - \strlen($payload) % 4) % 4); - echo "DEBUG access token claims: " . \base64_decode($payload) . "\n"; - } - } - return $tokenData; + return \json_decode($entry['value']); } } throw new Exception('Could not find an "oc_oAuth.user:" entry in the browser storage state.'); diff --git a/tests/acceptance/features/apiVault/vault.feature b/tests/acceptance/features/apiVault/vault.feature index 0785f78446f..47c7e59dc3f 100644 --- a/tests/acceptance/features/apiVault/vault.feature +++ b/tests/acceptance/features/apiVault/vault.feature @@ -21,47 +21,47 @@ Feature: vault | vaultFile.txt | - # Scenario: user can create folders and files in project space in vault - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Alice" has created a space "vault-space" in vault with the default quota using the Graph API - # When user "Alice" creates a folder "vaultFolder" in space "vault-space" in vault using the WebDav Api - # Then the HTTP status code should be "201" - # When user "Alice" uploads a file inside space "vault-space" with content "some content" to "vaultFile.txt" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And for user "Alice" the space "vault-space" in vault should contain these entries: - # | vaultFolder | - # | vaultFile.txt | + Scenario: user can create folders and files in project space in vault + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Alice" has created a space "vault-space" in vault with the default quota using the Graph API + When user "Alice" creates a folder "vaultFolder" in space "vault-space" in vault using the WebDav Api + Then the HTTP status code should be "201" + When user "Alice" uploads a file inside space "vault-space" with content "some content" to "vaultFile.txt" in vault using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" the space "vault-space" in vault should contain these entries: + | vaultFolder | + | vaultFile.txt | - # Scenario: resources in drive and vault are isolated - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "driveFolder" in space "Personal" - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFile.txt" - # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - # When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And for user "Alice" the space "Personal" in vault should contain these entries: - # | vaultFolder | - # | vaultFile.txt | - # And for user "Alice" the space "Personal" should contain these entries: - # | driveFolder | - # | driveFile.txt | - # And for user "Alice" the space "Personal" in vault should not contain these entries: - # | driveFolder | - # | driveFile.txt | - # And for user "Alice" the space "Personal" should not contain these entries: - # | vaultFolder | - # | vaultFile.txt | + Scenario: resources in drive and vault are isolated + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "driveFolder" in space "Personal" + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFile.txt" + And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" the space "Personal" in vault should contain these entries: + | vaultFolder | + | vaultFile.txt | + And for user "Alice" the space "Personal" should contain these entries: + | driveFolder | + | driveFile.txt | + And for user "Alice" the space "Personal" in vault should not contain these entries: + | driveFolder | + | driveFile.txt | + And for user "Alice" the space "Personal" should not contain these entries: + | vaultFolder | + | vaultFile.txt | - # @env-config @keycloak-config - # Scenario: user can set custom auth level names - # Given the administrator has set the Keycloak realm attribute "acr.loa.map" to '{"regular":"1","testing":"2"}' - # And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" - # And user "Alice" has logged in via web UI - # When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And user "Alice" should have a JWT token with an ACR value "testing" + @env-config @keycloak-config + Scenario: user can set custom auth level names + Given the administrator has set the Keycloak realm attribute "acr.loa.map" to '{"regular":"1","testing":"2"}' + And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" + And user "Alice" has logged in via web UI + When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API + Then the HTTP status code should be "201" + And user "Alice" should have a JWT token with an ACR value "testing" # Scenario: check capabilities endpoint for vault @@ -184,15 +184,15 @@ Feature: vault # """ - # Scenario: user copies folder from drive to vault - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "driveFolder" in space "Personal" - # When user "Alice" copies folder "driveFolder" from space "Personal" to "driveFolder" inside space "Personal" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And for user "Alice" the space "Personal" in vault should contain these entries: - # | driveFolder | - # And for user "Alice" the space "Personal" should contain these entries: - # | driveFolder | + Scenario: user copies folder from drive to vault + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "driveFolder" in space "Personal" + When user "Alice" copies folder "driveFolder" from space "Personal" to "driveFolder" inside space "Personal" in vault using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" the space "Personal" in vault should contain these entries: + | driveFolder | + And for user "Alice" the space "Personal" should contain these entries: + | driveFolder | # Scenario: user copies file from drive to vault diff --git a/tests/config/k8s/setup.sh b/tests/config/k8s/setup.sh index 5a95043ea14..adf1295e5e5 100644 --- a/tests/config/k8s/setup.sh +++ b/tests/config/k8s/setup.sh @@ -81,6 +81,9 @@ fi if [[ "$ENABLE_VAULT" == "true" ]]; then sed -i '/vault:/{n;s|false|true|}' $CFG_DIR/values.yaml + # bump from the default "error" level so the CreateHome-for-vault-personal + # and MFA-check Debug/Info log lines (silent at "error") are visible for debugging + sed -i '/^logging:/{n;s|level: error|level: debug|}' $CFG_DIR/values.yaml fi # copy custom values file diff --git a/tests/config/k8s/templates/extra.tpl b/tests/config/k8s/templates/extra.tpl index 63975593389..2f26521784b 100644 --- a/tests/config/k8s/templates/extra.tpl +++ b/tests/config/k8s/templates/extra.tpl @@ -126,6 +126,28 @@ value: storage-users-vault - name: STORAGE_USERS_EVENTS_CONSUMER_GROUP value: vault-dcfs +{{/* Without these overrides this instance falls back to the shared OCIS_CACHE_STORE=nats-js-kv + used by the regular storage-users service. Since personal-space IDs are just the user's + opaque ID (identical in both instances), the vault instance's existence check on + CreateStorageSpace sees a cache hit from whatever the regular instance already created for + that user and returns AlreadyExists without ever writing its own space - the vault personal + space then never actually exists on this instance, even though every check claims it does. */}} +- name: STORAGE_USERS_FILEMETADATA_CACHE_STORE + value: memory +- name: STORAGE_USERS_ID_CACHE_STORE + value: memory +{{/* services/storage-users/pkg/config/parser/parse.go calls EnsureDefaults() (which forces + MountID to the vault constant when EnableVaultMode is set) BEFORE envdecode.Decode() + applies env vars - so EnableVaultMode is still false at that point and the override never + fires. Without this, MountID falls back to whatever STORAGE_USERS_MOUNT_ID resolves to + (the "storage-uuid" ConfigMap, shared with the regular storage-users instance), so spaces + created here come back with the wrong storage id embedded in their space id. That id still + lists fine (gateway's registry matches on its own static rule, not this value), but any + later ID-based lookup (e.g. creating a folder inside a newly created project space) fails + to route back to this instance. Setting it directly here sidesteps the ordering bug + entirely, since env vars are applied regardless of EnsureDefaults. */}} +- name: STORAGE_USERS_MOUNT_ID + value: "1a01c2c4-4309-4483-a845-842fd56d8622" {{- end -}} {{- end -}} {{- end -}} diff --git a/tests/config/k8s/values.yaml b/tests/config/k8s/values.yaml index 5b4c69ec660..3baf0d30fc6 100644 --- a/tests/config/k8s/values.yaml +++ b/tests/config/k8s/values.yaml @@ -8,7 +8,7 @@ ingress: externalDomain: ocis-server logging: - level: error + level: debug insecure: oidcIdpInsecure: true ocisHttpApiInsecure: true @@ -17,7 +17,7 @@ features: authapp: enabled: false vault: - enabled: false + enabled: true emailNotifications: enabled: false smtp: From 72c28ab3857b346ce769f4f4d1a228f52c06953f Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Thu, 3 Sep 2026 10:50:39 +0545 Subject: [PATCH 05/16] test: log and retry until the deadline Signed-off-by: Prajwol Amatya --- tests/acceptance/bootstrap/OcisConfigContext.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/bootstrap/OcisConfigContext.php b/tests/acceptance/bootstrap/OcisConfigContext.php index 11276f6c977..9621a534ceb 100644 --- a/tests/acceptance/bootstrap/OcisConfigContext.php +++ b/tests/acceptance/bootstrap/OcisConfigContext.php @@ -393,6 +393,7 @@ private function assertOcisRestarted(ResponseInterface $response, string $errorM private function waitForOcisProxyReady(int $timeoutSeconds = 60): void { $readyzUrl = 'http://localhost:9205/readyz'; $deadline = time() + $timeoutSeconds; + $lastError = null; while (time() < $deadline) { try { $response = HttpRequestHelper::get($readyzUrl); @@ -401,12 +402,15 @@ private function waitForOcisProxyReady(int $timeoutSeconds = 60): void { } echo "oCIS not ready yet. Retrying in 1s...\n"; } catch (\Exception $e) { - throw new Exception("oCIS not ready. Error: $e"); + // connection errors are expected while oCIS is restarting, keep retrying until the deadline + $lastError = $e; + echo "oCIS not reachable yet ({$e->getMessage()}). Retrying in 1s...\n"; } sleep(1); } + $errorSuffix = $lastError ? " Last error: {$lastError->getMessage()}" : ""; throw new \RuntimeException( - "Timed out after {$timeoutSeconds}s waiting for oCIS proxy readyz at {$readyzUrl}", + "Timed out after {$timeoutSeconds}s waiting for oCIS proxy readyz at {$readyzUrl}.{$errorSuffix}", ); } From a393eaa35f7714a465f337f8a38a43003b2aff67 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Thu, 3 Sep 2026 11:05:57 +0545 Subject: [PATCH 06/16] test: expose proxy readyz Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 22 ++++++++++++++++++- .../bootstrap/OcisConfigContext.php | 13 +++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index c81da35adec..55fb6bd8c37 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -337,7 +337,26 @@ jobs: - name: Expose debug ports run: bash tests/config/k8s/expose-debug-svc.sh - + + # env-config scenarios (OcisConfigContext::waitForOcisProxyReady) poll the proxy's + # unauthenticated /readyz endpoint after every config-triggered restart. Local port 9205 + # is unavailable on the runner (k3d's loadbalancer reserves 9100-9399, see create-cluster), + # so forward it to 19205 instead, for the whole remainder of the job - unlike the earlier + # one-off port-forward in "Wait for oCIS to be ready", this one is intentionally left + # running (not killed via trap) since env-config scenarios can restart oCIS at any point + # during the test run. Wrapped in a reconnect loop: a config change that restarts the + # proxy pod itself would kill a plain `kubectl port-forward` for good, since it targets a + # specific pod and doesn't follow a deployment through a rollout. + - name: Expose proxy readyz + if: matrix.suite == 'apiVault' + run: | + ( + while true; do + kubectl -n ocis-server port-forward deployment/proxy 19205:9205 >> /tmp/proxy-readyz-portforward.log 2>&1 + sleep 1 + done + ) & + - name: Build ociswrapper run: make -C tests/ociswrapper/ @@ -386,6 +405,7 @@ jobs: K8S: ${{ env.K8S }} KEYCLOAK: ${{ matrix.suite == 'apiVault' }} KC_URL: https://keycloak:8443 + PROXY_READYZ_URL: ${{ matrix.suite == 'apiVault' && 'http://localhost:19205/readyz' || '' }} run: make test-acceptance-api - name: Debug storage-users-vault (post-test) diff --git a/tests/acceptance/bootstrap/OcisConfigContext.php b/tests/acceptance/bootstrap/OcisConfigContext.php index 9621a534ceb..81667d35f4d 100644 --- a/tests/acceptance/bootstrap/OcisConfigContext.php +++ b/tests/acceptance/bootstrap/OcisConfigContext.php @@ -391,9 +391,11 @@ private function assertOcisRestarted(ResponseInterface $response, string $errorM * @throws GuzzleException */ private function waitForOcisProxyReady(int $timeoutSeconds = 60): void { - $readyzUrl = 'http://localhost:9205/readyz'; + // In k8s, port 9205 on the runner's localhost is unavailable (k3d's loadbalancer + // reserves that range), so the proxy's debug port is forwarded to a different local + // port for the lifetime of the job. See k8s.yml's "Expose proxy readyz" step. + $readyzUrl = getenv('PROXY_READYZ_URL') ?: 'http://localhost:9205/readyz'; $deadline = time() + $timeoutSeconds; - $lastError = null; while (time() < $deadline) { try { $response = HttpRequestHelper::get($readyzUrl); @@ -402,15 +404,12 @@ private function waitForOcisProxyReady(int $timeoutSeconds = 60): void { } echo "oCIS not ready yet. Retrying in 1s...\n"; } catch (\Exception $e) { - // connection errors are expected while oCIS is restarting, keep retrying until the deadline - $lastError = $e; - echo "oCIS not reachable yet ({$e->getMessage()}). Retrying in 1s...\n"; + throw new Exception("oCIS not ready. Error: $e"); } sleep(1); } - $errorSuffix = $lastError ? " Last error: {$lastError->getMessage()}" : ""; throw new \RuntimeException( - "Timed out after {$timeoutSeconds}s waiting for oCIS proxy readyz at {$readyzUrl}.{$errorSuffix}", + "Timed out after {$timeoutSeconds}s waiting for oCIS proxy readyz at {$readyzUrl}", ); } From 9758ee481c5726c1175bf8df412fc4774b185311 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Thu, 3 Sep 2026 11:28:32 +0545 Subject: [PATCH 07/16] test: configure auth level name for proxy and frontend services Signed-off-by: Prajwol Amatya --- tests/acceptance/features/apiVault/vault.feature | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/acceptance/features/apiVault/vault.feature b/tests/acceptance/features/apiVault/vault.feature index 47c7e59dc3f..50e9a368dbe 100644 --- a/tests/acceptance/features/apiVault/vault.feature +++ b/tests/acceptance/features/apiVault/vault.feature @@ -57,7 +57,13 @@ Feature: vault @env-config @keycloak-config Scenario: user can set custom auth level names Given the administrator has set the Keycloak realm attribute "acr.loa.map" to '{"regular":"1","testing":"2"}' - And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" + # OCIS_MFA_AUTH_LEVEL_NAMES is read by both the proxy (mfa.go, gates access) and the + # frontend (exposed via the capabilities endpoint, which tells the web app which acr_values + # to request during step-up). In k8s each is a separate deployment, so both must be + # reconfigured explicitly - unlike the single-binary setup, there is no "just set it + # globally" here. + And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" for "proxy" service + And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" for "frontend" service And user "Alice" has logged in via web UI When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API Then the HTTP status code should be "201" From 064667946a7ebe3382505068e7e6015d6f446094 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Thu, 3 Sep 2026 11:52:09 +0545 Subject: [PATCH 08/16] test: generate admin access token if expired Signed-off-by: Prajwol Amatya --- .../acceptance/TestHelpers/KeycloakHelper.php | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/acceptance/TestHelpers/KeycloakHelper.php b/tests/acceptance/TestHelpers/KeycloakHelper.php index f9f256064e0..1bef48bf772 100644 --- a/tests/acceptance/TestHelpers/KeycloakHelper.php +++ b/tests/acceptance/TestHelpers/KeycloakHelper.php @@ -46,6 +46,7 @@ class KeycloakHelper { 'offline_access' => 'e2145b30-bf6f-49fb-af3f-1b40168bfcef', ]; private static ?string $adminAccessToken = null; + private static ?int $adminAccessTokenExpiry = null; /** * @return bool @@ -72,6 +73,7 @@ public static function getKeycloakUrl(): string { */ public static function setAdminAccessToken(string $accessToken): void { self::$adminAccessToken = $accessToken; + self::$adminAccessTokenExpiry = self::extractExpiry($accessToken); } /** @@ -81,6 +83,26 @@ public static function setAdminAccessToken(string $accessToken): void { */ public static function resetAdminAccessToken(): void { self::$adminAccessToken = null; + self::$adminAccessTokenExpiry = null; + } + + /** + * Decodes the "exp" claim from a JWT without verifying its signature - this token was just + * issued by Keycloak itself, so it is trusted here purely to know when to refresh it. + * + * @param string $jwt + * + * @return int|null + */ + private static function extractExpiry(string $jwt): ?int { + $segments = \explode('.', $jwt); + if (\count($segments) !== 3) { + return null; + } + $payload = \strtr($segments[1], '-_', '+/'); + $payload .= \str_repeat('=', (4 - \strlen($payload) % 4) % 4); + $decoded = \json_decode((string)\base64_decode($payload), true); + return \is_array($decoded) && isset($decoded['exp']) ? (int)$decoded['exp'] : null; } /** @@ -88,7 +110,10 @@ public static function resetAdminAccessToken(): void { * @throws GuzzleException */ public static function getAdminAccessToken(): string { - if (self::$adminAccessToken === null) { + // refresh a bit before the actual expiry so a token that's barely valid doesn't get used + // for a request that then takes a few seconds to reach the server + $expiringSoon = self::$adminAccessTokenExpiry !== null && self::$adminAccessTokenExpiry - 10 < \time(); + if (self::$adminAccessToken === null || $expiringSoon) { self::setAdminAccessToken(self::generateAdminAccessToken()); } From bbe30acd3cc54326f611860d6423bf2028fc31a5 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Thu, 3 Sep 2026 12:10:02 +0545 Subject: [PATCH 09/16] test: check ociswrapper logs Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 5 ++++- tests/acceptance/bootstrap/OcisConfigContext.php | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index 55fb6bd8c37..c9422dc57f8 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -367,7 +367,7 @@ jobs: --admin-username admin \ --admin-password admin \ --skip-ocis-run \ - -n ocis-server & + -n ocis-server > /tmp/ociswrapper.log 2>&1 & - name: Install Playwright for vault tests if: matrix.suite == 'apiVault' @@ -433,6 +433,9 @@ jobs: kubectl -n ocis-server exec "$VAULT_POD" -- find /var/lib/ocis -maxdepth 8 2>&1 || true fi + echo "--- ociswrapper: full log - shows exactly what happened during each env-config reconfigure/rollback for proxy and frontend, and how long each service actually took to report ready ---" + cat /tmp/ociswrapper.log 2>/dev/null || true + - name: Run Core ${{ matrix.suite }} tests if: startsWith( matrix.suite, 'core' ) env: diff --git a/tests/acceptance/bootstrap/OcisConfigContext.php b/tests/acceptance/bootstrap/OcisConfigContext.php index 81667d35f4d..f4353deec5c 100644 --- a/tests/acceptance/bootstrap/OcisConfigContext.php +++ b/tests/acceptance/bootstrap/OcisConfigContext.php @@ -335,6 +335,15 @@ public function theAdministratorHasStartedServiceSeparatelyWithTheFollowingConfi public function rollback(): void { if (OcisConfigHelper::isK8s()) { $this->rollbackK8sServices(); + // The wrapper's own readiness check (ociswrapper/ocis/k8s.go waitForService) only + // confirms the pod is healthy/ready - it does not confirm the proxy has finished + // (re-)fetching Keycloak's OIDC discovery document after a restart. A pod can report + // ready while that fetch is still in flight, so the very next scenario's login can + // hit a proxy that isn't actually ready to redirect through the OIDC flow yet. Poll + // the same unauthenticated readyz endpoint used elsewhere as an extra safety margin. + if (KeycloakHelper::isTestingWithKeycloak()) { + $this->waitForOcisProxyReady(); + } return; } $this->rollbackServices(); From 74fd03f24f71746cc12e762e89fa7440328706a1 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Thu, 3 Sep 2026 12:28:06 +0545 Subject: [PATCH 10/16] test: roll back to default auth level name Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 16 ++++++++++++++++ tests/acceptance/TestHelpers/WebUIHelper.php | 12 ++++++++++++ tests/ociswrapper/ocis/k8s.go | 14 ++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index c9422dc57f8..ce4f2d86322 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -436,6 +436,22 @@ jobs: echo "--- ociswrapper: full log - shows exactly what happened during each env-config reconfigure/rollback for proxy and frontend, and how long each service actually took to report ready ---" cat /tmp/ociswrapper.log 2>/dev/null || true + echo "--- proxy/frontend: actual live OCIS_MFA_AUTH_LEVEL_NAMES on the pod right now (checked directly, not via wrapper's own log which can truncate very long lines around 1KB) ---" + kubectl -n ocis-server exec deployment/proxy -- env | grep OCIS_MFA_AUTH_LEVEL_NAMES || echo "(not set on proxy - i.e. reverted to code default)" + kubectl -n ocis-server exec deployment/frontend -- env | grep OCIS_MFA_AUTH_LEVEL_NAMES || echo "(not set on frontend - i.e. reverted to code default)" + + echo "--- keycloak: actual current acr.loa.map realm attribute - was VaultContext::restoreKeycloakRealmAttributes() able to restore it? ---" + KC_ADMIN_TOKEN=$(curl -sk -X POST "https://keycloak:8443/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "client_id=admin-cli&username=admin&password=admin&grant_type=password" \ + | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + if [[ -n "$KC_ADMIN_TOKEN" ]]; then + curl -sk "https://keycloak:8443/admin/realms/oCIS" -H "Authorization: Bearer $KC_ADMIN_TOKEN" \ + | grep -o '"acr\.loa\.map":"[^}]*}"' || echo "(acr.loa.map attribute not found on realm)" + else + echo "(could not obtain keycloak admin token for this check)" + fi + - name: Run Core ${{ matrix.suite }} tests if: startsWith( matrix.suite, 'core' ) env: diff --git a/tests/acceptance/TestHelpers/WebUIHelper.php b/tests/acceptance/TestHelpers/WebUIHelper.php index 0a2730f5d59..f01d03362f9 100644 --- a/tests/acceptance/TestHelpers/WebUIHelper.php +++ b/tests/acceptance/TestHelpers/WebUIHelper.php @@ -99,6 +99,18 @@ public static function setUpUser(string $ocisUrl, string $username, string $pass $page->waitForSelector(self::$filesView, ['timeout' => self::$defaultTimeout]); return $context->storageState(); } catch (\Exception $e) { + // TEMPORARY DEBUG: on failure, capture what the browser was actually looking at - + // which page/URL it landed on and a snippet of the page content - since the + // exception message alone does not say whether it's stuck on a blank page, an OIDC + // error page, an unexpected Keycloak required-action page, or something else. + try { + $debugUrl = isset($page) ? $page->url() : '(no page)'; + $debugContent = isset($page) ? \substr($page->content(), 0, 2000) : '(no page)'; + echo "DEBUG login failure for '$username' - current URL: $debugUrl\n"; + echo "DEBUG login failure for '$username' - page content snippet: $debugContent\n"; + } catch (\Exception $debugException) { + echo "DEBUG login failure for '$username' - could not capture page state: " . $debugException->getMessage() . "\n"; + } throw new Exception("Login failed for user '$username': " . $e->getMessage(), 0, $e); } finally { $context->close(); diff --git a/tests/ociswrapper/ocis/k8s.go b/tests/ociswrapper/ocis/k8s.go index 7c1fb3e1b73..60b1cad8a9d 100644 --- a/tests/ociswrapper/ocis/k8s.go +++ b/tests/ociswrapper/ocis/k8s.go @@ -61,6 +61,19 @@ func K8sUpdateEnv(service string, envMap []string) (bool, string) { K8sOcisInitEnv[service].CurrentPod = podName } + // envMap may introduce vars that have no prior explicit value on the pod at all (e.g. only + // a code-level default was in effect) - the tracked baseline, just established/updated + // above by either branch, has no entry for those, so on its own it is not a rollback target + // that removes them; kubectl set env is additive and never strips a var it isn't told + // about. Mark any such brand-new var for removal now, while we still know it is new, or it + // silently survives every future rollback. + log.Println(fmt.Sprintf("[%s] DEBUG requested envMap: %s", service, strings.Join(envMap, ", "))) + log.Println(fmt.Sprintf("[%s] DEBUG baseline before newlyIntroduced check: %s", service, strings.Join(K8sOcisInitEnv[service].Envs, ", "))) + newlyIntroduced := diffEnvs(K8sOcisInitEnv[service].Envs, envMap) + log.Println(fmt.Sprintf("[%s] DEBUG newlyIntroduced: %s", service, strings.Join(newlyIntroduced, ", "))) + K8sOcisInitEnv[service].Envs = append(K8sOcisInitEnv[service].Envs, newlyIntroduced...) + log.Println(fmt.Sprintf("[%s] DEBUG baseline immediately after append, ptr=%p: %s", service, K8sOcisInitEnv[service], strings.Join(K8sOcisInitEnv[service].Envs, ", "))) + envSet, skipWaitForService, err := setServiceEnv(service, envMap, "Failed to set env") if err != nil { return false, "error setting env" @@ -328,6 +341,7 @@ func waitPodDelete(podName string, timeout int) (string, error) { func K8sRollback() (bool, string) { for service, config := range K8sOcisInitEnv { + log.Println(fmt.Sprintf("[%s] DEBUG config ptr=%p at top of rollback loop", service, config)) envs := config.Envs log.Println(fmt.Sprintf("[%s] Rolling envs: %s", service, strings.Join(envs, ", "))) podName, err := getPodName(service) From 418a251dcb38447b01016551527213027cdb9c45 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Fri, 4 Sep 2026 09:27:33 +0545 Subject: [PATCH 11/16] test: check if retry works Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 16 ++++++++ tests/acceptance/TestHelpers/WebUIHelper.php | 37 +++++++++++++++++-- .../bootstrap/OcisConfigContext.php | 9 ----- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index ce4f2d86322..120c607808e 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -440,6 +440,11 @@ jobs: kubectl -n ocis-server exec deployment/proxy -- env | grep OCIS_MFA_AUTH_LEVEL_NAMES || echo "(not set on proxy - i.e. reverted to code default)" kubectl -n ocis-server exec deployment/frontend -- env | grep OCIS_MFA_AUTH_LEVEL_NAMES || echo "(not set on frontend - i.e. reverted to code default)" + echo "--- proxy: actual live PROXY_OIDC_ISSUER on the pod right now - the chart renders this var TWICE (default, then vault override), so this checks how kubectl set env actually handled a pre-existing duplicate key during rollback ---" + kubectl -n ocis-server exec deployment/proxy -- env | grep PROXY_OIDC_ISSUER || echo "(PROXY_OIDC_ISSUER not set at all on proxy)" + echo "--- proxy: full deployment spec env list for PROXY_OIDC_ISSUER (shows if there are now 0, 1, or 2 entries in the spec itself, not just what env picks up) ---" + kubectl -n ocis-server get deployment proxy -o jsonpath='{.spec.template.spec.containers[0].env}' | grep -o '{"name":"PROXY_OIDC_ISSUER"[^}]*}' + echo "--- keycloak: actual current acr.loa.map realm attribute - was VaultContext::restoreKeycloakRealmAttributes() able to restore it? ---" KC_ADMIN_TOKEN=$(curl -sk -X POST "https://keycloak:8443/realms/master/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ @@ -452,6 +457,17 @@ jobs: echo "(could not obtain keycloak admin token for this check)" fi + echo "--- proxy: rewritten OIDC well-known document (this is what the web app fetches for OIDC discovery, PROXY_OIDC_REWRITE_WELLKNOWN=true) - is it actually serving correctly right now? ---" + echo "--- response headers ---" + curl -sk -D - -o /dev/null "https://ocis-server/.well-known/openid-configuration" || echo "(request failed outright)" + echo "--- response body ---" + curl -sk "https://ocis-server/.well-known/openid-configuration"; echo + echo "--- keycloak's own (non-rewritten) well-known, for comparison - is keycloak itself healthy? ---" + curl -sk -o /dev/null -w "HTTP_STATUS:%{http_code}\n" "https://keycloak:8443/realms/oCIS/.well-known/openid-configuration" + + echo "--- proxy log: error-level lines mentioning well-known / openid / discovery around this time ---" + grep -iE "well-known|discovery|openid.config" tests/config/k8s/logs/*.log 2>/dev/null | grep -i '"level":"error"' | tail -50 || true + - name: Run Core ${{ matrix.suite }} tests if: startsWith( matrix.suite, 'core' ) env: diff --git a/tests/acceptance/TestHelpers/WebUIHelper.php b/tests/acceptance/TestHelpers/WebUIHelper.php index f01d03362f9..187870ceed3 100644 --- a/tests/acceptance/TestHelpers/WebUIHelper.php +++ b/tests/acceptance/TestHelpers/WebUIHelper.php @@ -64,7 +64,25 @@ public static function setUpUser(string $ocisUrl, string $username, string $pass try { $page = $context->newPage(); $page->goto($ocisUrl, ['waitUntil' => 'networkidle']); - $page->waitForSelector(self::$keycloakHeader, ['timeout' => self::$defaultTimeout]); + // Right after a proxy/frontend pod restart (e.g. an env-config scenario's rollback), + // the web app can transiently land on its own /login interstitial instead of + // auto-redirecting to the OIDC provider, even though the pod already reports + // healthy - the client-side OIDC discovery call can still hit a connection to + // Keycloak that isn't fully warm yet. Retry the navigation a few times rather than + // failing outright on what is usually a few-second timing gap. + $maxAttempts = 3; + for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { + try { + $page->waitForSelector(self::$keycloakHeader, ['timeout' => self::$defaultTimeout]); + break; + } catch (\Exception $e) { + if ($attempt === $maxAttempts) { + throw $e; + } + \usleep(1000 * 1000); + $page->goto($ocisUrl, ['waitUntil' => 'networkidle']); + } + } $page->locator(self::$usernameInput)->fill($username); $page->locator(self::$passwordInput)->fill($password); $page->locator(self::$loginButton)->click(); @@ -105,9 +123,22 @@ public static function setUpUser(string $ocisUrl, string $username, string $pass // error page, an unexpected Keycloak required-action page, or something else. try { $debugUrl = isset($page) ? $page->url() : '(no page)'; - $debugContent = isset($page) ? \substr($page->content(), 0, 2000) : '(no page)'; echo "DEBUG login failure for '$username' - current URL: $debugUrl\n"; - echo "DEBUG login failure for '$username' - page content snippet: $debugContent\n"; + if (isset($page)) { + $fullContent = $page->content(); + // prefer centering the snippet on the actual error message the web app + // rendered (e.g. "Something went wrong") over just the raw start, + // since that's where the actual OIDC failure detail/reason should be + $errorPos = \strpos($fullContent, 'Something went wrong'); + if ($errorPos !== false) { + $start = max(0, $errorPos - 200); + $snippet = \substr($fullContent, $start, 6000); + } else { + $bodyPos = \strpos($fullContent, 'getMessage() . "\n"; } diff --git a/tests/acceptance/bootstrap/OcisConfigContext.php b/tests/acceptance/bootstrap/OcisConfigContext.php index f4353deec5c..81667d35f4d 100644 --- a/tests/acceptance/bootstrap/OcisConfigContext.php +++ b/tests/acceptance/bootstrap/OcisConfigContext.php @@ -335,15 +335,6 @@ public function theAdministratorHasStartedServiceSeparatelyWithTheFollowingConfi public function rollback(): void { if (OcisConfigHelper::isK8s()) { $this->rollbackK8sServices(); - // The wrapper's own readiness check (ociswrapper/ocis/k8s.go waitForService) only - // confirms the pod is healthy/ready - it does not confirm the proxy has finished - // (re-)fetching Keycloak's OIDC discovery document after a restart. A pod can report - // ready while that fetch is still in flight, so the very next scenario's login can - // hit a proxy that isn't actually ready to redirect through the OIDC flow yet. Poll - // the same unauthenticated readyz endpoint used elsewhere as an extra safety margin. - if (KeycloakHelper::isTestingWithKeycloak()) { - $this->waitForOcisProxyReady(); - } return; } $this->rollbackServices(); From 10de182e5b328932734afa0177181a272f4cdd84 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Fri, 4 Sep 2026 11:26:58 +0545 Subject: [PATCH 12/16] test: clean deduplicate environments Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 8 +- tests/acceptance/TestHelpers/WebUIHelper.php | 39 +- .../features/apiVault/vault.feature | 2034 ++++++++--------- tests/ociswrapper/ocis/k8s.go | 55 +- 4 files changed, 1098 insertions(+), 1038 deletions(-) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index 120c607808e..f58b3f147aa 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -443,7 +443,13 @@ jobs: echo "--- proxy: actual live PROXY_OIDC_ISSUER on the pod right now - the chart renders this var TWICE (default, then vault override), so this checks how kubectl set env actually handled a pre-existing duplicate key during rollback ---" kubectl -n ocis-server exec deployment/proxy -- env | grep PROXY_OIDC_ISSUER || echo "(PROXY_OIDC_ISSUER not set at all on proxy)" echo "--- proxy: full deployment spec env list for PROXY_OIDC_ISSUER (shows if there are now 0, 1, or 2 entries in the spec itself, not just what env picks up) ---" - kubectl -n ocis-server get deployment proxy -o jsonpath='{.spec.template.spec.containers[0].env}' | grep -o '{"name":"PROXY_OIDC_ISSUER"[^}]*}' + kubectl -n ocis-server get deployment proxy -o jsonpath='{.spec.template.spec.containers[0].env}' | grep -o '{"name":"PROXY_OIDC_ISSUER"[^}]*}' || true + + echo "--- proxy: other vault-mode-relevant env vars live on the pod right now, for Alice's access-denied failure ---" + kubectl -n ocis-server exec deployment/proxy -- env | grep -E "PROXY_AUTOPROVISION_ACCOUNTS|PROXY_ROLE_ASSIGNMENT_DRIVER|PROXY_USER_OIDC_CLAIM|PROXY_USER_CS3_CLAIM|OCIS_ENABLE_VAULT_MODE|OCIS_MFA_ENABLED" || true + + echo "--- proxy log: lines mentioning role assignment / autoprovision / access denied around Alice's login ---" + grep -iE "role assign|autoprovision|access.denied|permission denied" tests/config/k8s/logs/*.log 2>/dev/null | tail -80 || true echo "--- keycloak: actual current acr.loa.map realm attribute - was VaultContext::restoreKeycloakRealmAttributes() able to restore it? ---" KC_ADMIN_TOKEN=$(curl -sk -X POST "https://keycloak:8443/realms/master/protocol/openid-connect/token" \ diff --git a/tests/acceptance/TestHelpers/WebUIHelper.php b/tests/acceptance/TestHelpers/WebUIHelper.php index 187870ceed3..02f3e3e833f 100644 --- a/tests/acceptance/TestHelpers/WebUIHelper.php +++ b/tests/acceptance/TestHelpers/WebUIHelper.php @@ -64,25 +64,26 @@ public static function setUpUser(string $ocisUrl, string $username, string $pass try { $page = $context->newPage(); $page->goto($ocisUrl, ['waitUntil' => 'networkidle']); - // Right after a proxy/frontend pod restart (e.g. an env-config scenario's rollback), - // the web app can transiently land on its own /login interstitial instead of - // auto-redirecting to the OIDC provider, even though the pod already reports - // healthy - the client-side OIDC discovery call can still hit a connection to - // Keycloak that isn't fully warm yet. Retry the navigation a few times rather than - // failing outright on what is usually a few-second timing gap. - $maxAttempts = 3; - for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { - try { - $page->waitForSelector(self::$keycloakHeader, ['timeout' => self::$defaultTimeout]); - break; - } catch (\Exception $e) { - if ($attempt === $maxAttempts) { - throw $e; - } - \usleep(1000 * 1000); - $page->goto($ocisUrl, ['waitUntil' => 'networkidle']); - } - } + $page->waitForSelector(self::$keycloakHeader, ['timeout' => self::$defaultTimeout]); + // // Right after a proxy/frontend pod restart (e.g. an env-config scenario's rollback), + // // the web app can transiently land on its own /login interstitial instead of + // // auto-redirecting to the OIDC provider, even though the pod already reports + // // healthy - the client-side OIDC discovery call can still hit a connection to + // // Keycloak that isn't fully warm yet. Retry the navigation a few times rather than + // // failing outright on what is usually a few-second timing gap. + // $maxAttempts = 3; + // for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { + // try { + // $page->waitForSelector(self::$keycloakHeader, ['timeout' => self::$defaultTimeout]); + // break; + // } catch (\Exception $e) { + // if ($attempt === $maxAttempts) { + // throw $e; + // } + // \usleep(1000 * 1000); + // $page->goto($ocisUrl, ['waitUntil' => 'networkidle']); + // } + // } $page->locator(self::$usernameInput)->fill($username); $page->locator(self::$passwordInput)->fill($password); $page->locator(self::$loginButton)->click(); diff --git a/tests/acceptance/features/apiVault/vault.feature b/tests/acceptance/features/apiVault/vault.feature index 50e9a368dbe..a9cf7b42b55 100644 --- a/tests/acceptance/features/apiVault/vault.feature +++ b/tests/acceptance/features/apiVault/vault.feature @@ -70,124 +70,124 @@ Feature: vault And user "Alice" should have a JWT token with an ACR value "testing" - # Scenario: check capabilities endpoint for vault - # Given using OCS API version "2" - # And user "Alice" has logged in via web UI - # When user "Alice" retrieves the vault mode capabilities using the capabilities API - # Then the OCS status code should be "200" - # And the HTTP status code should be "200" - # And the ocs JSON data of the response should match - # """ - # { - # "type": "object", - # "required": [ "capabilities" ], - # "properties": { - # "capabilities": { - # "type": "object", - # "required": [ - # "core", - # "files", - # "files_sharing", - # "auth", - # "vault" - # ], - # "properties": { - # "files_sharing": { - # "type": "object", - # "required": [ - # "api_enabled", - # "default_permissions", - # "public", - # "resharing", - # "federation", - # "group_sharing", - # "share_with_group_members_only", - # "share_with_membership_groups_only", - # "auto_accept_share", - # "user_enumeration" - # ], - # "properties": { - # "federation": { - # "type": "object", - # "required": [ - # "outgoing", - # "incoming" - # ], - # "properties": { - # "outgoing": { - # "const": false - # }, - # "incoming": { - # "const": false - # } - # } - # }, - # "public": { - # "type": "object", - # "required": [ - # "enabled", - # "multiple", - # "upload", - # "supports_upload_only", - # "send_mail", - # "social_share" - # ], - # "properties": { - # "enabled": { - # "const": false - # } - # } - # } - # } - # }, - # "auth": { - # "type": "object", - # "required": [ - # "mfa" - # ], - # "properties": { - # "mfa": { - # "type": "object", - # "required": [ - # "enabled", - # "levelnames" - # ], - # "properties": { - # "enabled": { - # "const": true - # }, - # "levelnames": { - # "type": "array", - # "minItems": 1, - # "maxItems": 1, - # "items": { - # "const": "advanced" - # } - # } - # } - # } - # } - # }, - # "vault": { - # "type": "object", - # "required": [ - # "enabled", - # "vault_storage_provider" - # ], - # "properties": { - # "enabled": { - # "const": true - # }, - # "vault_storage_provider": { - # "pattern": "%uuidv4_pattern%" - # } - # } - # } - # } - # } - # } - # } - # """ + Scenario: check capabilities endpoint for vault + Given using OCS API version "2" + And user "Alice" has logged in via web UI + When user "Alice" retrieves the vault mode capabilities using the capabilities API + Then the OCS status code should be "200" + And the HTTP status code should be "200" + And the ocs JSON data of the response should match + """ + { + "type": "object", + "required": [ "capabilities" ], + "properties": { + "capabilities": { + "type": "object", + "required": [ + "core", + "files", + "files_sharing", + "auth", + "vault" + ], + "properties": { + "files_sharing": { + "type": "object", + "required": [ + "api_enabled", + "default_permissions", + "public", + "resharing", + "federation", + "group_sharing", + "share_with_group_members_only", + "share_with_membership_groups_only", + "auto_accept_share", + "user_enumeration" + ], + "properties": { + "federation": { + "type": "object", + "required": [ + "outgoing", + "incoming" + ], + "properties": { + "outgoing": { + "const": false + }, + "incoming": { + "const": false + } + } + }, + "public": { + "type": "object", + "required": [ + "enabled", + "multiple", + "upload", + "supports_upload_only", + "send_mail", + "social_share" + ], + "properties": { + "enabled": { + "const": false + } + } + } + } + }, + "auth": { + "type": "object", + "required": [ + "mfa" + ], + "properties": { + "mfa": { + "type": "object", + "required": [ + "enabled", + "levelnames" + ], + "properties": { + "enabled": { + "const": true + }, + "levelnames": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "const": "advanced" + } + } + } + } + } + }, + "vault": { + "type": "object", + "required": [ + "enabled", + "vault_storage_provider" + ], + "properties": { + "enabled": { + "const": true + }, + "vault_storage_provider": { + "pattern": "%uuidv4_pattern%" + } + } + } + } + } + } + } + """ Scenario: user copies folder from drive to vault @@ -201,905 +201,905 @@ Feature: vault | driveFolder | - # Scenario: user copies file from drive to vault - # Given user "Alice" has logged in via web UI - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" - # When user "Alice" copies file "testfile.txt" from space "Personal" to "testfile.txt" inside space "Personal" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And for user "Alice" the content of the file "testfile.txt" of the space "Personal" in vault should be "some content" - # And for user "Alice" the space "Personal" should contain these entries: - # | testfile.txt | - - - # Scenario: user tries to copy folder from vault to drive - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - # When user "Alice" copies folder "vaultFolder" from space "Personal" in vault to "vaultFolder" inside space "Personal" using the WebDAV API - # Then the HTTP status code should be "409" - # And for user "Alice" the space "Personal" should not contain these entries: - # | vaultFolder | - # And for user "Alice" the space "Personal" in vault should contain these entries: - # | vaultFolder | - - - # Scenario: user tries to copy file from vault to drive - # Given user "Alice" has logged in via web UI - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" in vault - # When user "Alice" copies file "testfile.txt" from space "Personal" in vault to "testfile.txt" inside space "Personal" using the WebDAV API - # Then the HTTP status code should be "409" - # And for user "Alice" the space "Personal" should not contain these entries: - # | testfile.txt | - # And for user "Alice" the space "Personal" in vault should contain these entries: - # | testfile.txt | - - - # Scenario: user copies sub-folder from drive to vault - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "driveFolder" in space "Personal" - # And user "Alice" has created a folder "driveFolder/subFolder" in space "Personal" - # When user "Alice" copies folder "driveFolder/subFolder" from space "Personal" to "subFolder" inside space "Personal" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And for user "Alice" the space "Personal" in vault should contain these entries: - # | subFolder | - # And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: - # | subFolder | - - - # Scenario: user copies file inside folder from drive to vault - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "driveFolder" in space "Personal" - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFolder/testfile.txt" - # When user "Alice" copies file "driveFolder/testfile.txt" from space "Personal" to "testfile.txt" inside space "Personal" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And for user "Alice" the content of the file "testfile.txt" of the space "Personal" in vault should be "some content" - # And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: - # | testfile.txt | - - - # Scenario: user copies sub-folder from drive to a folder in vault - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "driveFolder" in space "Personal" - # And user "Alice" has created a folder "driveFolder/subFolder" in space "Personal" - # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - # When user "Alice" copies folder "driveFolder/subFolder" from space "Personal" to "vaultFolder/subFolder" inside space "Personal" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And for user "Alice" folder "vaultFolder" of the space "Personal" in vault should contain these entries: - # | subFolder | - # And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: - # | subFolder | - - - # Scenario: user copies file inside folder from drive to a folder in vault - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "driveFolder" in space "Personal" - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFolder/testfile.txt" - # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - # When user "Alice" copies file "driveFolder/testfile.txt" from space "Personal" to "vaultFolder/testfile.txt" inside space "Personal" in vault using the WebDAV API - # Then the HTTP status code should be "201" - # And for user "Alice" the content of the file "vaultFolder/testfile.txt" of the space "Personal" in vault should be "some content" - # And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: - # | testfile.txt | - - - # Scenario: user tries to create a public link of a folder inside vault - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - # When user "Alice" creates the following resource link share using the Graph API: - # | resource | vaultFolder | - # | space | Personal | - # | permissionsRole | View | - # | storage | vault | - # Then the HTTP status code should be "400" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": ["error"], - # "properties": { - # "error": { - # "type": "object", - # "required": ["code", "innererror", "message"], - # "properties": { - # "code": { - # "const": "invalidRequest" - # }, - # "innererror": { - # "type": "object", - # "required": [ - # "date", - # "request-id" - # ] - # }, - # "message": { - # "const": "public links are not allowed for vault resources" - # } - # } - # } - # } - # } - # """ - - - # Scenario: user tries to create a public link of a file inside vault - # Given user "Alice" has logged in via web UI - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" in vault - # When user "Alice" creates the following resource link share using the Graph API: - # | resource | testfile.txt | - # | space | Personal | - # | permissionsRole | View | - # | storage | vault | - # Then the HTTP status code should be "400" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": ["error"], - # "properties": { - # "error": { - # "type": "object", - # "required": ["code", "innererror", "message"], - # "properties": { - # "code": { - # "const": "invalidRequest" - # }, - # "innererror": { - # "type": "object", - # "required": [ - # "date", - # "request-id" - # ] - # }, - # "message": { - # "const": "public links are not allowed for vault resources" - # } - # } - # } - # } - # } - # """ - - - # Scenario: user tries to create a public link of a space root inside vault - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Alice" has created a space "vault-space" in vault with the default quota using the Graph API - # When user "Alice" tries to create the following space link share using permissions endpoint of the Graph API: - # | space | vault-space | - # | permissionsRole | View | - # | storage | vault | - # Then the HTTP status code should be "400" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": ["error"], - # "properties": { - # "error": { - # "type": "object", - # "required": ["code", "innererror", "message"], - # "properties": { - # "code": { - # "const": "invalidRequest" - # }, - # "innererror": { - # "type": "object", - # "required": [ - # "date", - # "request-id" - # ] - # }, - # "message": { - # "const": "public links are not allowed for vault resources" - # } - # } - # } - # } - # } - # """ - - - # Scenario Outline: send share invitation for project space in vault to user with different roles (permissions endpoint) - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Brian" has been created with default attributes - # And user "Brian" has logged in via web UI - # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - # | space | new-space | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "200" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": [ - # "value" - # ], - # "properties": { - # "value": { - # "type": "array", - # "minItems": 1, - # "maxItems": 1, - # "items": { - # "type": "object", - # "required": [ - # "grantedToV2", - # "roles" - # ], - # "properties": { - # "grantedToV2": { - # "type": "object", - # "required": [ - # "user" - # ], - # "properties": { - # "user": { - # "type": "object", - # "required": [ - # "displayName", - # "id" - # ], - # "properties": { - # "displayName": { - # "const": "Brian Murphy" - # }, - # "id": { - # "type": "string", - # "pattern": "^%user_id_pattern%$" - # } - # } - # } - # } - # }, - # "roles": { - # "type": "array", - # "minItems": 1, - # "maxItems": 1, - # "items": { - # "type": "string", - # "pattern": "^%role_id_pattern%$" - # } - # } - # } - # } - # } - # } - # } - # """ - # Examples: - # | permissions-role | - # | Space Viewer | - # | Space Editor | - # | Manager | - - - # Scenario Outline: send share invitation for disabled project space in vault to user with different roles (permissions endpoint) - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Brian" has been created with default attributes - # And user "Brian" has logged in via web UI - # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - # And user "Admin" has disabled a space "new-space" in vault - # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - # | space | new-space | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "404" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": [ - # "error" - # ], - # "properties": { - # "error": { - # "type": "object", - # "required": [ - # "code", - # "message" - # ], - # "properties": { - # "code": { - # "const": "itemNotFound" - # }, - # "message": { - # "type": "string", - # "pattern": "^stat: error: not found: %user_id_pattern%$" - # } - # } - # } - # } - # } - # """ - # Examples: - # | permissions-role | - # | Space Viewer | - # | Space Editor | - # | Manager | - - - # Scenario Outline: send share invitation for deleted project space in vault to user with different roles (permissions endpoint) - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Brian" has been created with default attributes - # And user "Brian" has logged in via web UI - # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - # And user "Admin" has disabled a space "new-space" in vault - # And user "Admin" has deleted a space "new-space" in vault - # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - # | space | new-space | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "404" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": [ - # "error" - # ], - # "properties": { - # "error": { - # "type": "object", - # "required": [ - # "code", - # "message" - # ], - # "properties": { - # "code": { - # "const": "itemNotFound" - # }, - # "message": { - # "const": "stat: error: not found: " - # } - # } - # } - # } - # } - # """ - # Examples: - # | permissions-role | - # | Space Viewer | - # | Space Editor | - # | Manager | - - - # Scenario Outline: try to send share invitation for personal space in vault to user with different roles (permissions endpoint) - # Given user "Alice" has logged in via web UI - # And user "Brian" has been created with default attributes - # And user "Brian" has logged in via web UI - # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - # | space | Personal | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "400" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": ["error"], - # "properties": { - # "error": { - # "type": "object", - # "required": ["code", "innererror", "message"], - # "properties": { - # "code": { - # "const": "invalidRequest" - # }, - # "innererror": { - # "type": "object", - # "required": [ - # "date", - # "request-id" - # ] - # }, - # "message": { - # "const": "space type is not eligible for sharing" - # } - # } - # } - # } - # } - # """ - # Examples: - # | permissions-role | - # | Space Viewer | - # | Space Editor | - # | Manager | - - - # Scenario Outline: try to share Shares space in vault with a user (permissions endpoint) - # Given user "Alice" has logged in via web UI - # And user "Brian" has been created with default attributes - # And user "Brian" has logged in via web UI - # When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: - # | space | Shares | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "400" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": ["error"], - # "properties": { - # "error": { - # "type": "object", - # "required": ["code", "innererror", "message"], - # "properties": { - # "code": { - # "const": "invalidRequest" - # }, - # "innererror": { - # "type": "object", - # "required": [ - # "date", - # "request-id" - # ] - # }, - # "message": { - # "const": "" - # } - # } - # } - # } - # } - # """ - # Examples: - # | permissions-role | error-message | - # | Space Viewer | role not applicable to this resource | - # | Space Editor | role not applicable to this resource | - # | Manager | role not applicable to this resource | - - - # Scenario Outline: invite user to a project space in vault with different roles using root endpoint - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Brian" has been created with default attributes - # And user "Brian" has logged in via web UI - # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - # When user "Alice" sends the following space share invitation using root endpoint of the Graph API: - # | space | new-space | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "200" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": [ - # "value" - # ], - # "properties": { - # "value": { - # "type": "array", - # "minItems": 1, - # "maxItems": 1, - # "items": { - # "type": "object", - # "required": [ - # "grantedToV2", - # "roles" - # ], - # "properties": { - # "grantedToV2": { - # "type": "object", - # "required": [ - # "user" - # ], - # "properties": { - # "user": { - # "type": "object", - # "required": [ - # "displayName", - # "id" - # ], - # "properties": { - # "displayName": { - # "type": "string", - # "const": "Brian Murphy" - # }, - # "id": { - # "type": "string", - # "pattern": "^%user_id_pattern%$" - # } - # } - # } - # } - # }, - # "roles": { - # "type": "array", - # "minItems": 1, - # "maxItems": 1, - # "items": { - # "type": "string", - # "pattern": "^%role_id_pattern%$" - # } - # } - # } - # } - # } - # } - # } - # """ - # Examples: - # | permissions-role | - # | Space Viewer | - # | Space Editor | - # | Manager | - - - # Scenario Outline: try to invite user to personal drive in vault with different roles using root endpoint - # Given user "Alice" has logged in via web UI - # And user "Brian" has been created with default attributes - # And user "Brian" has logged in via web UI - # When user "Alice" tries to send the following space share invitation using root endpoint of the Graph API: - # | space | Personal | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "400" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": ["error"], - # "properties": { - # "error": { - # "type": "object", - # "required": ["code", "innererror", "message"], - # "properties": { - # "code": { - # "const": "invalidRequest" - # }, - # "innererror": { - # "type": "object", - # "required": [ - # "date", - # "request-id" - # ] - # }, - # "message": { - # "const": "unsupported space type" - # } - # } - # } - # } - # } - # """ - # Examples: - # | permissions-role | - # | Space Viewer | - # | Space Editor | - # | Manager | - - - # Scenario Outline: try to invite user to Shares drive in vault with different roles using root endpoint - # Given user "Alice" has logged in via web UI - # And user "Brian" has been created with default attributes - # And user "Brian" has logged in via web UI - # When user "Alice" tries to send the following space share invitation using root endpoint of the Graph API: - # | space | Shares | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "400" - # And the JSON data of the response should match - # """ - # { - # "type": "object", - # "required": ["error"], - # "properties": { - # "error": { - # "type": "object", - # "required": ["code", "innererror", "message"], - # "properties": { - # "code": { - # "const": "invalidRequest" - # }, - # "innererror": { - # "type": "object", - # "required": [ - # "date", - # "request-id" - # ] - # }, - # "message": { - # "const": "unsupported space type" - # } - # } - # } - # } - # } - # """ - # Examples: - # | permissions-role | - # | Space Viewer | - # | Space Editor | - # | Manager | - - - # Scenario: search results for resources in Personal space should be isolated between vault and drive - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "testDriveFolder" in space "Personal" - # And user "Alice" has created a folder "testVaultFolder" in space "Personal" in vault - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testDriveFile.txt" - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testVaultFile.txt" in vault - # When user "Alice" searches for "*test*" inside space "Personal" in vault using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "2" entries - # And the search result of user "Alice" should contain only these entries: - # | testVaultFolder | - # | testVaultFile.txt | - # When user "Alice" searches for "*test*" inside space "Personal" using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "2" entries - # And the search result of user "Alice" should contain only these entries: - # | testDriveFolder | - # | testDriveFile.txt | - - - # Scenario: search results for resources inside folder with same name should be isolated between vault and drive - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "newFolder" in space "Personal" - # And user "Alice" has created a folder "newFolder" in space "Personal" in vault - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "newFolder/testDriveFile.txt" - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "newFolder/testVaultFile.txt" in vault - # When user "Alice" searches for "*test*" inside folder "newFolder" in space "Personal" in vault using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "1" entries - # And the search result of user "Alice" should contain only these entries: - # | newFolder/testVaultFile.txt | - # When user "Alice" searches for "*test*" inside folder "newFolder" in space "Personal" using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "1" entries - # And the search result of user "Alice" should contain only these entries: - # | newFolder/testDriveFile.txt | - - - # Scenario: search result for resources inside project spaces with same name should be isolated between vault and drive - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Alice" has created a space "new-space" with the default quota using the Graph API - # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - # And user "Alice" has created a folder "testDriveFolder" in space "new-space" - # And user "Alice" has created a folder "testVaultFolder" in space "new-space" in vault - # And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testDriveFile.txt" - # And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testVaultFile.txt" in vault - # When user "Alice" searches for "*test*" inside space "new-space" in vault using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "2" entries - # And the search result of user "Alice" should contain only these entries: - # | testVaultFolder | - # | testVaultFile.txt | - # When user "Alice" searches for "*test*" inside space "new-space" using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "2" entries - # And the search result of user "Alice" should contain only these entries: - # | testDriveFolder | - # | testDriveFile.txt | - - # @tikaServiceNeeded - # Scenario: search result by content of file should be isolated between vault and drive - # Given user "Alice" has logged in via web UI - # And user "Alice" has uploaded a file inside space "Personal" with content "content of file in drive" to "testDriveFile.txt" - # And user "Alice" has uploaded a file inside space "Personal" with content "content of file in vault" to "testVaultFile.txt" in vault - # When user "Alice" searches for "Content:content" in vault using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "1" entries - # And the search result of user "Alice" should contain only these files: - # | testVaultFile.txt | - # When user "Alice" searches for "Content:content" using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "1" entries - # And the search result of user "Alice" should contain only these files: - # | testDriveFile.txt | - - # @tikaServiceNeeded - # Scenario: search result by content of file inside project space should be isolated between vault and drive - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Alice" has created a space "new-space" with the default quota using the Graph API - # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - # And user "Alice" has uploaded a file inside space "new-space" with content "content of file in drive" to "testDriveFile.txt" - # And user "Alice" has uploaded a file inside space "new-space" with content "content of file in vault" to "testVaultFile.txt" in vault - # When user "Alice" searches for "Content:content" in vault using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "1" entries - # And the search result of user "Alice" should contain only these files: - # | testVaultFile.txt | - # When user "Alice" searches for "Content:content" using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "1" entries - # And the search result of user "Alice" should contain only these files: - # | testDriveFile.txt | - - - # Scenario: search results by resource tags should be isolated between vault and drive - # Given user "Alice" has logged in via web UI - # And user "Alice" has created a folder "driveFolder" in space "Personal" - # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testDriveFile.txt" - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testVaultFile.txt" in vault - # And user "Alice" has tagged the following files of the space "Personal": - # | path | tagName | - # | testDriveFile.txt | tag1 | - # And user "Alice" has tagged the following folders of the space "Personal": - # | path | tagName | - # | driveFolder | tag1 | - # And user "Alice" has tagged the following files of the space "Personal" in vault: - # | path | tagName | - # | testVaultFile.txt | tag1 | - # And user "Alice" has tagged the following folders of the space "Personal" in vault: - # | path | tagName | - # | vaultFolder | tag1 | - # When user "Alice" searches for "Tags:tag1" in vault using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "2" entries - # And the search result of user "Alice" should contain only these files: - # | testVaultFile.txt | - # | vaultFolder | - # When user "Alice" searches for "Tags:tag1" using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "2" entries - # And the search result of user "Alice" should contain only these files: - # | testDriveFile.txt | - # | driveFolder | - - - # Scenario: search results by resource tags inside project space should be isolated between vault and drive - # Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Alice" has created a space "new-space" with the default quota using the Graph API - # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - # And user "Alice" has created a folder "driveFolder" in space "new-space" - # And user "Alice" has created a folder "vaultFolder" in space "new-space" in vault - # And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testDriveFile.txt" - # And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testVaultFile.txt" in vault - # And user "Alice" has tagged the following files of the space "new-space": - # | path | tagName | - # | testDriveFile.txt | tag1 | - # And user "Alice" has tagged the following folders of the space "new-space": - # | path | tagName | - # | driveFolder | tag1 | - # And user "Alice" has tagged the following files of the space "new-space" in vault: - # | path | tagName | - # | testVaultFile.txt | tag1 | - # And user "Alice" has tagged the following folders of the space "new-space" in vault: - # | path | tagName | - # | vaultFolder | tag1 | - # When user "Alice" searches for "Tags:tag1" in vault using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "2" entries - # And the search result of user "Alice" should contain only these files: - # | testVaultFile.txt | - # | vaultFolder | - # When user "Alice" searches for "Tags:tag1" using the WebDAV API - # Then the HTTP status code should be "207" - # And the search result should contain "2" entries - # And the search result of user "Alice" should contain only these files: - # | testDriveFile.txt | - # | driveFolder | - - - # Scenario Outline: folder share received from vault and drive personal space should be isolated - # Given user "Brian" has been created with default attributes - # And user "Alice" has logged in via web UI - # And user "Brian" has logged in via web UI - # And user "Alice" has created a folder "driveFolder" in space "Personal" - # And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault - # And user "Alice" has sent the following resource share invitation: - # | resource | driveFolder | - # | space | Personal | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # When user "Alice" sends the following resource share invitation using the Graph API: - # | resource | vaultFolder | - # | space | Personal | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "200" - # And user "Brian" should have a share in vault "vaultFolder" synced - # And user "Brian" should have the following resource shares: - # | resource | permissionsRole | sharer | space | storage | - # | vaultFolder | | Alice | Personal | vault | - # And user "Brian" should have the following resource shares: - # | resource | permissionsRole | sharer | space | - # | driveFolder | | Alice | Personal | - # Examples: - # | permissions-role | - # | Viewer | - # | Editor | - # | Uploader | - - - # Scenario Outline: file share received from vault and drive personal space should be isolated - # Given user "Brian" has been created with default attributes - # And user "Alice" has logged in via web UI - # And user "Brian" has logged in via web UI - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFile.txt" - # And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault - # And user "Alice" has sent the following resource share invitation: - # | resource | driveFile.txt | - # | space | Personal | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # When user "Alice" sends the following resource share invitation using the Graph API: - # | resource | vaultFile.txt | - # | space | Personal | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "200" - # And user "Brian" should have a share in vault "vaultFile.txt" synced - # And user "Brian" should have the following resource shares: - # | resource | permissionsRole | sharer | space | storage | - # | vaultFile.txt | | Alice | Personal | vault | - # And user "Brian" should have the following resource shares: - # | resource | permissionsRole | sharer | space | - # | driveFile.txt | | Alice | Personal | - # Examples: - # | permissions-role | - # | Viewer | - # | File Editor | - - - # Scenario Outline: folder share received from vault and drive project space should be isolated - # Given user "Brian" has been created with default attributes - # And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API - # And user "Alice" has logged in via web UI - # And user "Brian" has logged in via web UI - # And user "Alice" has created a space "new-space" with the default quota using the Graph API - # And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API - # And user "Alice" has created a folder "driveFolder" in space "new-space" - # And user "Alice" has created a folder "vaultFolder" in space "new-space" in vault - # And user "Alice" has sent the following resource share invitation: - # | resource | driveFolder | - # | space | new-space | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # When user "Alice" sends the following resource share invitation using the Graph API: - # | resource | vaultFolder | - # | space | new-space | - # | sharee | Brian | - # | shareType | user | - # | permissionsRole | | - # | storage | vault | - # Then the HTTP status code should be "200" - # And user "Brian" should have a share in vault "vaultFolder" synced - # And user "Brian" should have the following resource shares: - # | resource | permissionsRole | sharer | space | storage | - # | vaultFolder | | Alice | new-space | vault | - # And user "Brian" should have the following resource shares: - # | resource | permissionsRole | sharer | space | - # | driveFolder | | Alice | new-space | - # Examples: - # | permissions-role | - # | Viewer | - # | Editor | - # | Uploader | + Scenario: user copies file from drive to vault + Given user "Alice" has logged in via web UI + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" + When user "Alice" copies file "testfile.txt" from space "Personal" to "testfile.txt" inside space "Personal" in vault using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" the content of the file "testfile.txt" of the space "Personal" in vault should be "some content" + And for user "Alice" the space "Personal" should contain these entries: + | testfile.txt | + + + Scenario: user tries to copy folder from vault to drive + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + When user "Alice" copies folder "vaultFolder" from space "Personal" in vault to "vaultFolder" inside space "Personal" using the WebDAV API + Then the HTTP status code should be "409" + And for user "Alice" the space "Personal" should not contain these entries: + | vaultFolder | + And for user "Alice" the space "Personal" in vault should contain these entries: + | vaultFolder | + + + Scenario: user tries to copy file from vault to drive + Given user "Alice" has logged in via web UI + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" in vault + When user "Alice" copies file "testfile.txt" from space "Personal" in vault to "testfile.txt" inside space "Personal" using the WebDAV API + Then the HTTP status code should be "409" + And for user "Alice" the space "Personal" should not contain these entries: + | testfile.txt | + And for user "Alice" the space "Personal" in vault should contain these entries: + | testfile.txt | + + + Scenario: user copies sub-folder from drive to vault + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "driveFolder" in space "Personal" + And user "Alice" has created a folder "driveFolder/subFolder" in space "Personal" + When user "Alice" copies folder "driveFolder/subFolder" from space "Personal" to "subFolder" inside space "Personal" in vault using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" the space "Personal" in vault should contain these entries: + | subFolder | + And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: + | subFolder | + + + Scenario: user copies file inside folder from drive to vault + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "driveFolder" in space "Personal" + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFolder/testfile.txt" + When user "Alice" copies file "driveFolder/testfile.txt" from space "Personal" to "testfile.txt" inside space "Personal" in vault using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" the content of the file "testfile.txt" of the space "Personal" in vault should be "some content" + And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: + | testfile.txt | + + + Scenario: user copies sub-folder from drive to a folder in vault + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "driveFolder" in space "Personal" + And user "Alice" has created a folder "driveFolder/subFolder" in space "Personal" + And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + When user "Alice" copies folder "driveFolder/subFolder" from space "Personal" to "vaultFolder/subFolder" inside space "Personal" in vault using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" folder "vaultFolder" of the space "Personal" in vault should contain these entries: + | subFolder | + And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: + | subFolder | + + + Scenario: user copies file inside folder from drive to a folder in vault + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "driveFolder" in space "Personal" + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFolder/testfile.txt" + And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + When user "Alice" copies file "driveFolder/testfile.txt" from space "Personal" to "vaultFolder/testfile.txt" inside space "Personal" in vault using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" the content of the file "vaultFolder/testfile.txt" of the space "Personal" in vault should be "some content" + And for user "Alice" folder "driveFolder" of the space "Personal" should contain these entries: + | testfile.txt | + + + Scenario: user tries to create a public link of a folder inside vault + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + When user "Alice" creates the following resource link share using the Graph API: + | resource | vaultFolder | + | space | Personal | + | permissionsRole | View | + | storage | vault | + Then the HTTP status code should be "400" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": { + "type": "object", + "required": ["code", "innererror", "message"], + "properties": { + "code": { + "const": "invalidRequest" + }, + "innererror": { + "type": "object", + "required": [ + "date", + "request-id" + ] + }, + "message": { + "const": "public links are not allowed for vault resources" + } + } + } + } + } + """ + + + Scenario: user tries to create a public link of a file inside vault + Given user "Alice" has logged in via web UI + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testfile.txt" in vault + When user "Alice" creates the following resource link share using the Graph API: + | resource | testfile.txt | + | space | Personal | + | permissionsRole | View | + | storage | vault | + Then the HTTP status code should be "400" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": { + "type": "object", + "required": ["code", "innererror", "message"], + "properties": { + "code": { + "const": "invalidRequest" + }, + "innererror": { + "type": "object", + "required": [ + "date", + "request-id" + ] + }, + "message": { + "const": "public links are not allowed for vault resources" + } + } + } + } + } + """ + + + Scenario: user tries to create a public link of a space root inside vault + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Alice" has created a space "vault-space" in vault with the default quota using the Graph API + When user "Alice" tries to create the following space link share using permissions endpoint of the Graph API: + | space | vault-space | + | permissionsRole | View | + | storage | vault | + Then the HTTP status code should be "400" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": { + "type": "object", + "required": ["code", "innererror", "message"], + "properties": { + "code": { + "const": "invalidRequest" + }, + "innererror": { + "type": "object", + "required": [ + "date", + "request-id" + ] + }, + "message": { + "const": "public links are not allowed for vault resources" + } + } + } + } + } + """ + + + Scenario Outline: send share invitation for project space in vault to user with different roles (permissions endpoint) + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Brian" has been created with default attributes + And user "Brian" has logged in via web UI + And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + | space | new-space | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": [ + "grantedToV2", + "roles" + ], + "properties": { + "grantedToV2": { + "type": "object", + "required": [ + "user" + ], + "properties": { + "user": { + "type": "object", + "required": [ + "displayName", + "id" + ], + "properties": { + "displayName": { + "const": "Brian Murphy" + }, + "id": { + "type": "string", + "pattern": "^%user_id_pattern%$" + } + } + } + } + }, + "roles": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "string", + "pattern": "^%role_id_pattern%$" + } + } + } + } + } + } + } + """ + Examples: + | permissions-role | + | Space Viewer | + | Space Editor | + | Manager | + + + Scenario Outline: send share invitation for disabled project space in vault to user with different roles (permissions endpoint) + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Brian" has been created with default attributes + And user "Brian" has logged in via web UI + And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + And user "Admin" has disabled a space "new-space" in vault + When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + | space | new-space | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "404" + And the JSON data of the response should match + """ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "const": "itemNotFound" + }, + "message": { + "type": "string", + "pattern": "^stat: error: not found: %user_id_pattern%$" + } + } + } + } + } + """ + Examples: + | permissions-role | + | Space Viewer | + | Space Editor | + | Manager | + + + Scenario Outline: send share invitation for deleted project space in vault to user with different roles (permissions endpoint) + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Brian" has been created with default attributes + And user "Brian" has logged in via web UI + And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + And user "Admin" has disabled a space "new-space" in vault + And user "Admin" has deleted a space "new-space" in vault + When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + | space | new-space | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "404" + And the JSON data of the response should match + """ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "const": "itemNotFound" + }, + "message": { + "const": "stat: error: not found: " + } + } + } + } + } + """ + Examples: + | permissions-role | + | Space Viewer | + | Space Editor | + | Manager | + + + Scenario Outline: try to send share invitation for personal space in vault to user with different roles (permissions endpoint) + Given user "Alice" has logged in via web UI + And user "Brian" has been created with default attributes + And user "Brian" has logged in via web UI + When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + | space | Personal | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "400" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": { + "type": "object", + "required": ["code", "innererror", "message"], + "properties": { + "code": { + "const": "invalidRequest" + }, + "innererror": { + "type": "object", + "required": [ + "date", + "request-id" + ] + }, + "message": { + "const": "space type is not eligible for sharing" + } + } + } + } + } + """ + Examples: + | permissions-role | + | Space Viewer | + | Space Editor | + | Manager | + + + Scenario Outline: try to share Shares space in vault with a user (permissions endpoint) + Given user "Alice" has logged in via web UI + And user "Brian" has been created with default attributes + And user "Brian" has logged in via web UI + When user "Alice" sends the following space share invitation using permissions endpoint of the Graph API: + | space | Shares | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "400" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": { + "type": "object", + "required": ["code", "innererror", "message"], + "properties": { + "code": { + "const": "invalidRequest" + }, + "innererror": { + "type": "object", + "required": [ + "date", + "request-id" + ] + }, + "message": { + "const": "" + } + } + } + } + } + """ + Examples: + | permissions-role | error-message | + | Space Viewer | role not applicable to this resource | + | Space Editor | role not applicable to this resource | + | Manager | role not applicable to this resource | + + + Scenario Outline: invite user to a project space in vault with different roles using root endpoint + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Brian" has been created with default attributes + And user "Brian" has logged in via web UI + And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + When user "Alice" sends the following space share invitation using root endpoint of the Graph API: + | space | new-space | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "required": [ + "grantedToV2", + "roles" + ], + "properties": { + "grantedToV2": { + "type": "object", + "required": [ + "user" + ], + "properties": { + "user": { + "type": "object", + "required": [ + "displayName", + "id" + ], + "properties": { + "displayName": { + "type": "string", + "const": "Brian Murphy" + }, + "id": { + "type": "string", + "pattern": "^%user_id_pattern%$" + } + } + } + } + }, + "roles": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "string", + "pattern": "^%role_id_pattern%$" + } + } + } + } + } + } + } + """ + Examples: + | permissions-role | + | Space Viewer | + | Space Editor | + | Manager | + + + Scenario Outline: try to invite user to personal drive in vault with different roles using root endpoint + Given user "Alice" has logged in via web UI + And user "Brian" has been created with default attributes + And user "Brian" has logged in via web UI + When user "Alice" tries to send the following space share invitation using root endpoint of the Graph API: + | space | Personal | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "400" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": { + "type": "object", + "required": ["code", "innererror", "message"], + "properties": { + "code": { + "const": "invalidRequest" + }, + "innererror": { + "type": "object", + "required": [ + "date", + "request-id" + ] + }, + "message": { + "const": "unsupported space type" + } + } + } + } + } + """ + Examples: + | permissions-role | + | Space Viewer | + | Space Editor | + | Manager | + + + Scenario Outline: try to invite user to Shares drive in vault with different roles using root endpoint + Given user "Alice" has logged in via web UI + And user "Brian" has been created with default attributes + And user "Brian" has logged in via web UI + When user "Alice" tries to send the following space share invitation using root endpoint of the Graph API: + | space | Shares | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "400" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["error"], + "properties": { + "error": { + "type": "object", + "required": ["code", "innererror", "message"], + "properties": { + "code": { + "const": "invalidRequest" + }, + "innererror": { + "type": "object", + "required": [ + "date", + "request-id" + ] + }, + "message": { + "const": "unsupported space type" + } + } + } + } + } + """ + Examples: + | permissions-role | + | Space Viewer | + | Space Editor | + | Manager | + + + Scenario: search results for resources in Personal space should be isolated between vault and drive + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "testDriveFolder" in space "Personal" + And user "Alice" has created a folder "testVaultFolder" in space "Personal" in vault + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testDriveFile.txt" + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testVaultFile.txt" in vault + When user "Alice" searches for "*test*" inside space "Personal" in vault using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "2" entries + And the search result of user "Alice" should contain only these entries: + | testVaultFolder | + | testVaultFile.txt | + When user "Alice" searches for "*test*" inside space "Personal" using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "2" entries + And the search result of user "Alice" should contain only these entries: + | testDriveFolder | + | testDriveFile.txt | + + + Scenario: search results for resources inside folder with same name should be isolated between vault and drive + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "newFolder" in space "Personal" + And user "Alice" has created a folder "newFolder" in space "Personal" in vault + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "newFolder/testDriveFile.txt" + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "newFolder/testVaultFile.txt" in vault + When user "Alice" searches for "*test*" inside folder "newFolder" in space "Personal" in vault using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "1" entries + And the search result of user "Alice" should contain only these entries: + | newFolder/testVaultFile.txt | + When user "Alice" searches for "*test*" inside folder "newFolder" in space "Personal" using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "1" entries + And the search result of user "Alice" should contain only these entries: + | newFolder/testDriveFile.txt | + + + Scenario: search result for resources inside project spaces with same name should be isolated between vault and drive + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Alice" has created a space "new-space" with the default quota using the Graph API + And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + And user "Alice" has created a folder "testDriveFolder" in space "new-space" + And user "Alice" has created a folder "testVaultFolder" in space "new-space" in vault + And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testDriveFile.txt" + And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testVaultFile.txt" in vault + When user "Alice" searches for "*test*" inside space "new-space" in vault using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "2" entries + And the search result of user "Alice" should contain only these entries: + | testVaultFolder | + | testVaultFile.txt | + When user "Alice" searches for "*test*" inside space "new-space" using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "2" entries + And the search result of user "Alice" should contain only these entries: + | testDriveFolder | + | testDriveFile.txt | + + @tikaServiceNeeded + Scenario: search result by content of file should be isolated between vault and drive + Given user "Alice" has logged in via web UI + And user "Alice" has uploaded a file inside space "Personal" with content "content of file in drive" to "testDriveFile.txt" + And user "Alice" has uploaded a file inside space "Personal" with content "content of file in vault" to "testVaultFile.txt" in vault + When user "Alice" searches for "Content:content" in vault using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "1" entries + And the search result of user "Alice" should contain only these files: + | testVaultFile.txt | + When user "Alice" searches for "Content:content" using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "1" entries + And the search result of user "Alice" should contain only these files: + | testDriveFile.txt | + + @tikaServiceNeeded + Scenario: search result by content of file inside project space should be isolated between vault and drive + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Alice" has created a space "new-space" with the default quota using the Graph API + And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + And user "Alice" has uploaded a file inside space "new-space" with content "content of file in drive" to "testDriveFile.txt" + And user "Alice" has uploaded a file inside space "new-space" with content "content of file in vault" to "testVaultFile.txt" in vault + When user "Alice" searches for "Content:content" in vault using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "1" entries + And the search result of user "Alice" should contain only these files: + | testVaultFile.txt | + When user "Alice" searches for "Content:content" using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "1" entries + And the search result of user "Alice" should contain only these files: + | testDriveFile.txt | + + + Scenario: search results by resource tags should be isolated between vault and drive + Given user "Alice" has logged in via web UI + And user "Alice" has created a folder "driveFolder" in space "Personal" + And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testDriveFile.txt" + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "testVaultFile.txt" in vault + And user "Alice" has tagged the following files of the space "Personal": + | path | tagName | + | testDriveFile.txt | tag1 | + And user "Alice" has tagged the following folders of the space "Personal": + | path | tagName | + | driveFolder | tag1 | + And user "Alice" has tagged the following files of the space "Personal" in vault: + | path | tagName | + | testVaultFile.txt | tag1 | + And user "Alice" has tagged the following folders of the space "Personal" in vault: + | path | tagName | + | vaultFolder | tag1 | + When user "Alice" searches for "Tags:tag1" in vault using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "2" entries + And the search result of user "Alice" should contain only these files: + | testVaultFile.txt | + | vaultFolder | + When user "Alice" searches for "Tags:tag1" using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "2" entries + And the search result of user "Alice" should contain only these files: + | testDriveFile.txt | + | driveFolder | + + + Scenario: search results by resource tags inside project space should be isolated between vault and drive + Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Alice" has created a space "new-space" with the default quota using the Graph API + And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + And user "Alice" has created a folder "driveFolder" in space "new-space" + And user "Alice" has created a folder "vaultFolder" in space "new-space" in vault + And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testDriveFile.txt" + And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "testVaultFile.txt" in vault + And user "Alice" has tagged the following files of the space "new-space": + | path | tagName | + | testDriveFile.txt | tag1 | + And user "Alice" has tagged the following folders of the space "new-space": + | path | tagName | + | driveFolder | tag1 | + And user "Alice" has tagged the following files of the space "new-space" in vault: + | path | tagName | + | testVaultFile.txt | tag1 | + And user "Alice" has tagged the following folders of the space "new-space" in vault: + | path | tagName | + | vaultFolder | tag1 | + When user "Alice" searches for "Tags:tag1" in vault using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "2" entries + And the search result of user "Alice" should contain only these files: + | testVaultFile.txt | + | vaultFolder | + When user "Alice" searches for "Tags:tag1" using the WebDAV API + Then the HTTP status code should be "207" + And the search result should contain "2" entries + And the search result of user "Alice" should contain only these files: + | testDriveFile.txt | + | driveFolder | + + + Scenario Outline: folder share received from vault and drive personal space should be isolated + Given user "Brian" has been created with default attributes + And user "Alice" has logged in via web UI + And user "Brian" has logged in via web UI + And user "Alice" has created a folder "driveFolder" in space "Personal" + And user "Alice" has created a folder "vaultFolder" in space "Personal" in vault + And user "Alice" has sent the following resource share invitation: + | resource | driveFolder | + | space | Personal | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + When user "Alice" sends the following resource share invitation using the Graph API: + | resource | vaultFolder | + | space | Personal | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "200" + And user "Brian" should have a share in vault "vaultFolder" synced + And user "Brian" should have the following resource shares: + | resource | permissionsRole | sharer | space | storage | + | vaultFolder | | Alice | Personal | vault | + And user "Brian" should have the following resource shares: + | resource | permissionsRole | sharer | space | + | driveFolder | | Alice | Personal | + Examples: + | permissions-role | + | Viewer | + | Editor | + | Uploader | + + + Scenario Outline: file share received from vault and drive personal space should be isolated + Given user "Brian" has been created with default attributes + And user "Alice" has logged in via web UI + And user "Brian" has logged in via web UI + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "driveFile.txt" + And user "Alice" has uploaded a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault + And user "Alice" has sent the following resource share invitation: + | resource | driveFile.txt | + | space | Personal | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + When user "Alice" sends the following resource share invitation using the Graph API: + | resource | vaultFile.txt | + | space | Personal | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "200" + And user "Brian" should have a share in vault "vaultFile.txt" synced + And user "Brian" should have the following resource shares: + | resource | permissionsRole | sharer | space | storage | + | vaultFile.txt | | Alice | Personal | vault | + And user "Brian" should have the following resource shares: + | resource | permissionsRole | sharer | space | + | driveFile.txt | | Alice | Personal | + Examples: + | permissions-role | + | Viewer | + | File Editor | + + + Scenario Outline: folder share received from vault and drive project space should be isolated + Given user "Brian" has been created with default attributes + And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API + And user "Alice" has logged in via web UI + And user "Brian" has logged in via web UI + And user "Alice" has created a space "new-space" with the default quota using the Graph API + And user "Alice" has created a space "new-space" in vault with the default quota using the Graph API + And user "Alice" has created a folder "driveFolder" in space "new-space" + And user "Alice" has created a folder "vaultFolder" in space "new-space" in vault + And user "Alice" has sent the following resource share invitation: + | resource | driveFolder | + | space | new-space | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + When user "Alice" sends the following resource share invitation using the Graph API: + | resource | vaultFolder | + | space | new-space | + | sharee | Brian | + | shareType | user | + | permissionsRole | | + | storage | vault | + Then the HTTP status code should be "200" + And user "Brian" should have a share in vault "vaultFolder" synced + And user "Brian" should have the following resource shares: + | resource | permissionsRole | sharer | space | storage | + | vaultFolder | | Alice | new-space | vault | + And user "Brian" should have the following resource shares: + | resource | permissionsRole | sharer | space | + | driveFolder | | Alice | new-space | + Examples: + | permissions-role | + | Viewer | + | Editor | + | Uploader | Scenario Outline: folder share received from vault and drive project space should be isolated diff --git a/tests/ociswrapper/ocis/k8s.go b/tests/ociswrapper/ocis/k8s.go index 60b1cad8a9d..261b8061f70 100644 --- a/tests/ociswrapper/ocis/k8s.go +++ b/tests/ociswrapper/ocis/k8s.go @@ -139,7 +139,29 @@ func getInitialEnvs(service string) ([]string, error) { flatEnvVars = append(flatEnvVars, fmt.Sprintf("%s=%s", env.Name, env.Value)) } } - return flatEnvVars, nil + // The chart legitimately renders some vars twice in the pod spec (a default value, then a + // later override for the same name - Go's own os.Environ() takes the last one, which is + // what the running process actually observes). Deduping here, keeping the last occurrence, + // ensures this list is safe to replay through a single `kubectl set env` call later (e.g. + // during rollback): passing the same key twice in one invocation against a spec that + // already has two entries for it has been observed to drop the variable entirely instead of + // converging on one value, rather than raising an error. + return dedupeEnvs(flatEnvVars), nil +} + +func dedupeEnvs(envs []string) []string { + indexByKey := make(map[string]int, len(envs)) + deduped := make([]string, 0, len(envs)) + for _, env := range envs { + key := strings.SplitN(env, "=", 2)[0] + if idx, ok := indexByKey[key]; ok { + deduped[idx] = env + continue + } + indexByKey[key] = len(deduped) + deduped = append(deduped, env) + } + return deduped } func waitForService(service string, waitDeletion bool) (bool, error) { @@ -198,6 +220,37 @@ func waitForService(service string, waitDeletion bool) (bool, error) { } func setServiceEnv(service string, envMap []string, errMsgPrefix string) (bool, bool, error) { + // kubectl set env has been observed to behave unreliably (silently dropping the variable + // entirely, or picking an arbitrary one of the existing values) when the target pod spec + // already has multiple pre-existing entries for a key being set - which happens here + // because the chart legitimately renders some vars twice (a default value, then a later + // vault-mode override, relying on the running process's own last-one-wins env handling, + // not on kubectl ever reconciling it). Strip any existing entries for every key this call + // touches first, in its own invocation, so the actual set below always starts from a clean + // (zero-or-one-entry) state instead of leaving kubectl to reconcile a pre-existing + // duplicate on its own. Removing a key that isn't set is a no-op, so this is safe to do + // unconditionally. + removalArgs := []string{} + seenKeys := map[string]bool{} + for _, env := range envMap { + key := strings.TrimSuffix(strings.SplitN(env, "=", 2)[0], "-") + if !seenKeys[key] { + seenKeys[key] = true + removalArgs = append(removalArgs, key+"-") + } + } + if len(removalArgs) > 0 { + removeCmdArgs := append([]string{"set", "env", "-n", config.Get("namespace"), "deployment", service}, removalArgs...) + if _, err := exec.Command("kubectl", removeCmdArgs...).Output(); err != nil { + errMsg := "" + if exitErr, ok := err.(*exec.ExitError); ok { + errMsg = strings.TrimSpace(string(exitErr.Stderr)) + } + log.Println(fmt.Sprintf("[%s] Failed to pre-remove existing envs before setting them. %s", service, errMsg)) + return false, true, fmt.Errorf("error removing existing env before set") + } + } + cmdArgs := append([]string{"set", "env", "-n", config.Get("namespace"), "deployment", service}, envMap...) cmd := exec.Command("kubectl", cmdArgs...) output, err := cmd.Output() From 7cfc62cb7f955d8b9fea58262dcc4f447a29fda1 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Fri, 4 Sep 2026 14:13:38 +0545 Subject: [PATCH 13/16] test: revert changes Signed-off-by: Prajwol Amatya --- .github/workflows/acceptance-tests.yml | 2 +- .github/workflows/k8s.yml | 182 +++++------------- .../acceptance/TestHelpers/KeycloakHelper.php | 27 +-- tests/acceptance/TestHelpers/WebUIHelper.php | 44 ----- tests/config/k8s/setup.sh | 3 - tests/config/k8s/values.yaml | 4 +- tests/ociswrapper/ocis/k8s.go | 5 - 7 files changed, 48 insertions(+), 219 deletions(-) diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index f5b75fe56e8..84daf2deabd 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -1,7 +1,7 @@ name: Acceptance Tests on: - # pull_request: + pull_request: workflow_dispatch: jobs: diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index f58b3f147aa..246cdd05780 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -31,48 +31,48 @@ jobs: fail-fast: false matrix: include: - # - suite: apiContract - # - suite: apiLocks - # - suite: apiSettings - # # - suite: apiNotification - # - suite: apiCors - # - suite: apiGraphUser - # - suite: apiGraph - # - suite: apiGraphGroup - # # - suite: apiSpaces - # - suite: apiSpacesShares - # - suite: apiSpacesDavOperation - # - suite: apiDownloads - # - suite: apiAsyncUpload - # - suite: apiDepthInfinity - # - suite: apiArchiver - # - suite: apiActivities - # - suite: apiSearch1 - # - suite: apiSearch2 - # # - suite: apiSearchContent - # - suite: apiSharingNgShares - # - suite: apiReshare - # - suite: apiSharingNgPermissions - # - suite: apiSharingNgAdditionalShareRole - # - suite: apiSharingNgDriveInvitation - # # - suite: apiSharingNgItemInvitation - # - suite: apiSharingNgDriveLinkShare - # - suite: apiSharingNgItemLinkShare - # - suite: apiSharingNgLinkShareManagement - # - suite: apiAuthApp - # # - suite: apiAntivirus - # - suite: apiOcm - # - suite: apiCollaboration + - suite: apiContract + - suite: apiLocks + - suite: apiSettings + # - suite: apiNotification + - suite: apiCors + - suite: apiGraphUser + - suite: apiGraph + - suite: apiGraphGroup + # - suite: apiSpaces + - suite: apiSpacesShares + - suite: apiSpacesDavOperation + - suite: apiDownloads + - suite: apiAsyncUpload + - suite: apiDepthInfinity + - suite: apiArchiver + - suite: apiActivities + - suite: apiSearch1 + - suite: apiSearch2 + # - suite: apiSearchContent + - suite: apiSharingNgShares + - suite: apiReshare + - suite: apiSharingNgPermissions + - suite: apiSharingNgAdditionalShareRole + - suite: apiSharingNgDriveInvitation + # - suite: apiSharingNgItemInvitation + - suite: apiSharingNgDriveLinkShare + - suite: apiSharingNgItemLinkShare + - suite: apiSharingNgLinkShareManagement + - suite: apiAuthApp + # - suite: apiAntivirus + - suite: apiOcm + - suite: apiCollaboration - suite: apiVault - # # - suite: "coreApiAuth,coreApiCapabilities,coreApiFavorites,coreApiMain,coreApiVersions" - # - suite: "coreApiShareManagementBasicToShares,coreApiShareManagementToShares" - # - suite: "coreApiSharees" - # - suite: "coreApiSharePublicLink2" - # - suite: "coreApiShareOperationsToShares1,coreApiShareOperationsToShares2,coreApiSharePublicLink1,coreApiShareCreateSpecialToShares1,coreApiShareCreateSpecialToShares2,coreApiShareUpdateToShares" - # - suite: "coreApiTrashbin,coreApiTrashbinRestore,coreApiWebdavEtagPropagation1,coreApiWebdavEtagPropagation2" - # - suite: "coreApiWebdavDelete,coreApiWebdavOperations,coreApiWebdavMove2" - # - suite: "coreApiWebdavProperties" - # # - suite: "coreApiWebdavMove1,coreApiWebdavPreviews,coreApiWebdavUpload,coreApiWebdavUploadTUS" + # - suite: "coreApiAuth,coreApiCapabilities,coreApiFavorites,coreApiMain,coreApiVersions" + - suite: "coreApiShareManagementBasicToShares,coreApiShareManagementToShares" + - suite: "coreApiSharees" + - suite: "coreApiSharePublicLink2" + - suite: "coreApiShareOperationsToShares1,coreApiShareOperationsToShares2,coreApiSharePublicLink1,coreApiShareCreateSpecialToShares1,coreApiShareCreateSpecialToShares2,coreApiShareUpdateToShares" + - suite: "coreApiTrashbin,coreApiTrashbinRestore,coreApiWebdavEtagPropagation1,coreApiWebdavEtagPropagation2" + - suite: "coreApiWebdavDelete,coreApiWebdavOperations,coreApiWebdavMove2" + - suite: "coreApiWebdavProperties" + # - suite: "coreApiWebdavMove1,coreApiWebdavPreviews,coreApiWebdavUpload,coreApiWebdavUploadTUS" steps: - name: Checkout @@ -126,8 +126,8 @@ jobs: OCM=true make prepare-charts fi - # Keycloak/postgres (and the other suite-specific backends below) must be up and - # exposed to the cluster *before* oCIS is deployed: the proxy validates the + # Keycloak/postgres (and the other suite-specific backends) must be up and + # exposed to the cluster before oCIS is deployed: the proxy validates the # OCIS_OIDC_ISSUER well-known endpoint against Keycloak at startup in vault mode, # so `helm install --wait` would time out waiting for the proxy pod otherwise. - name: Deploy Suite-Specific External Backends @@ -307,34 +307,6 @@ jobs: retry "delete file (HTTP 204)" check_status 204 delete_file || exit 1 fi - - name: Debug storage-users-vault - if: always() && matrix.suite == 'apiVault' - run: | - echo "=== deployment/pods (label app=storageusers-vault) ===" - kubectl get deployment,pods -n ocis-server -l app=storageusers-vault -o wide || true - - echo "=== describe deployment storageusers-vault ===" - kubectl -n ocis-server describe deployment storageusers-vault || true - - echo "=== service storageusers-vault ===" - kubectl get svc storageusers-vault -n ocis-server -o wide || true - - echo "=== kubectl logs (likely empty: OCIS_LOG_FILE redirects to a file, not stdout) ===" - kubectl -n ocis-server logs deployment/storageusers-vault --tail=200 || true - kubectl -n ocis-server logs deployment/storageusers-vault --previous --tail=200 || true - - echo "=== real logs: every service in this chart writes to the same hostPath-mounted file ===" - echo "=== tests/config/k8s/logs/ocis.log (mounted into the k3d node at /logs, see create-cluster) ===" - ls -la tests/config/k8s/logs/ || true - echo "--- lines mentioning storage-users-vault, panic, fatal, or error ---" - grep -iE "storage-users-vault|panic|fatal|error" tests/config/k8s/logs/*.log 2>/dev/null | tail -200 || true - - echo "=== gateway lines mentioning vault ===" - grep -i "vault" tests/config/k8s/logs/*.log 2>/dev/null | grep -i gateway | tail -100 || true - - echo "=== features.vault.enabled in the values file actually used to deploy ===" - grep -A1 "vault:" tests/config/k8s/values.yaml || true - - name: Expose debug ports run: bash tests/config/k8s/expose-debug-svc.sh @@ -367,7 +339,7 @@ jobs: --admin-username admin \ --admin-password admin \ --skip-ocis-run \ - -n ocis-server > /tmp/ociswrapper.log 2>&1 & + -n ocis-server & - name: Install Playwright for vault tests if: matrix.suite == 'apiVault' @@ -408,72 +380,6 @@ jobs: PROXY_READYZ_URL: ${{ matrix.suite == 'apiVault' && 'http://localhost:19205/readyz' || '' }} run: make test-acceptance-api - - name: Debug storage-users-vault (post-test) - if: always() && matrix.suite == 'apiVault' - run: | - echo "=== deployment/pods (label app=storageusers-vault) ===" - kubectl get deployment,pods -n ocis-server -l app=storageusers-vault -o wide || true - - echo "--- proxy: CreateHome-for-vault-personal outcomes (create_home.go), full/untruncated ---" - grep -iE "CreateHome for vault personal|vault personal space creation failed|vault personal space (created|already exists)" tests/config/k8s/logs/*.log 2>/dev/null || true - - echo "--- graph: 'could not create drive' errors (drives.go CreateDrive handler) - did creating the project space itself actually fail server-side despite a 201? ---" - grep -iE "could not create drive" tests/config/k8s/logs/*.log 2>/dev/null || true - - echo "--- storage-users-vault: CreateStorageSpace / CreateContainer rgrpc calls with their result code - is the project space create + the later folder create both landing on this instance, and do they succeed? ---" - grep -iE '"service":"storage-users-vault".*(CreateStorageSpace|CreateContainer)' tests/config/k8s/logs/*.log 2>/dev/null || true - - echo "--- webdav/ocdav: FAILED_PRECONDITION / intermediate collection does not exist - the actual source of the 409 on folder creation (mkcol.go), distinct from AlreadyExists which maps to 405 ---" - grep -iE "FAILED_PRECONDITION|intermediate collection does not exist" tests/config/k8s/logs/*.log 2>/dev/null || true - - echo "--- storage-users-vault: actual on-disk layout at end of run ---" - VAULT_POD=$(kubectl -n ocis-server get pods -l app=storageusers-vault -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) - if [[ -n "$VAULT_POD" ]]; then - echo "pod: $VAULT_POD" - kubectl -n ocis-server exec "$VAULT_POD" -- find /var/lib/ocis -maxdepth 8 2>&1 || true - fi - - echo "--- ociswrapper: full log - shows exactly what happened during each env-config reconfigure/rollback for proxy and frontend, and how long each service actually took to report ready ---" - cat /tmp/ociswrapper.log 2>/dev/null || true - - echo "--- proxy/frontend: actual live OCIS_MFA_AUTH_LEVEL_NAMES on the pod right now (checked directly, not via wrapper's own log which can truncate very long lines around 1KB) ---" - kubectl -n ocis-server exec deployment/proxy -- env | grep OCIS_MFA_AUTH_LEVEL_NAMES || echo "(not set on proxy - i.e. reverted to code default)" - kubectl -n ocis-server exec deployment/frontend -- env | grep OCIS_MFA_AUTH_LEVEL_NAMES || echo "(not set on frontend - i.e. reverted to code default)" - - echo "--- proxy: actual live PROXY_OIDC_ISSUER on the pod right now - the chart renders this var TWICE (default, then vault override), so this checks how kubectl set env actually handled a pre-existing duplicate key during rollback ---" - kubectl -n ocis-server exec deployment/proxy -- env | grep PROXY_OIDC_ISSUER || echo "(PROXY_OIDC_ISSUER not set at all on proxy)" - echo "--- proxy: full deployment spec env list for PROXY_OIDC_ISSUER (shows if there are now 0, 1, or 2 entries in the spec itself, not just what env picks up) ---" - kubectl -n ocis-server get deployment proxy -o jsonpath='{.spec.template.spec.containers[0].env}' | grep -o '{"name":"PROXY_OIDC_ISSUER"[^}]*}' || true - - echo "--- proxy: other vault-mode-relevant env vars live on the pod right now, for Alice's access-denied failure ---" - kubectl -n ocis-server exec deployment/proxy -- env | grep -E "PROXY_AUTOPROVISION_ACCOUNTS|PROXY_ROLE_ASSIGNMENT_DRIVER|PROXY_USER_OIDC_CLAIM|PROXY_USER_CS3_CLAIM|OCIS_ENABLE_VAULT_MODE|OCIS_MFA_ENABLED" || true - - echo "--- proxy log: lines mentioning role assignment / autoprovision / access denied around Alice's login ---" - grep -iE "role assign|autoprovision|access.denied|permission denied" tests/config/k8s/logs/*.log 2>/dev/null | tail -80 || true - - echo "--- keycloak: actual current acr.loa.map realm attribute - was VaultContext::restoreKeycloakRealmAttributes() able to restore it? ---" - KC_ADMIN_TOKEN=$(curl -sk -X POST "https://keycloak:8443/realms/master/protocol/openid-connect/token" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "client_id=admin-cli&username=admin&password=admin&grant_type=password" \ - | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') - if [[ -n "$KC_ADMIN_TOKEN" ]]; then - curl -sk "https://keycloak:8443/admin/realms/oCIS" -H "Authorization: Bearer $KC_ADMIN_TOKEN" \ - | grep -o '"acr\.loa\.map":"[^}]*}"' || echo "(acr.loa.map attribute not found on realm)" - else - echo "(could not obtain keycloak admin token for this check)" - fi - - echo "--- proxy: rewritten OIDC well-known document (this is what the web app fetches for OIDC discovery, PROXY_OIDC_REWRITE_WELLKNOWN=true) - is it actually serving correctly right now? ---" - echo "--- response headers ---" - curl -sk -D - -o /dev/null "https://ocis-server/.well-known/openid-configuration" || echo "(request failed outright)" - echo "--- response body ---" - curl -sk "https://ocis-server/.well-known/openid-configuration"; echo - echo "--- keycloak's own (non-rewritten) well-known, for comparison - is keycloak itself healthy? ---" - curl -sk -o /dev/null -w "HTTP_STATUS:%{http_code}\n" "https://keycloak:8443/realms/oCIS/.well-known/openid-configuration" - - echo "--- proxy log: error-level lines mentioning well-known / openid / discovery around this time ---" - grep -iE "well-known|discovery|openid.config" tests/config/k8s/logs/*.log 2>/dev/null | grep -i '"level":"error"' | tail -50 || true - - name: Run Core ${{ matrix.suite }} tests if: startsWith( matrix.suite, 'core' ) env: diff --git a/tests/acceptance/TestHelpers/KeycloakHelper.php b/tests/acceptance/TestHelpers/KeycloakHelper.php index 1bef48bf772..f9f256064e0 100644 --- a/tests/acceptance/TestHelpers/KeycloakHelper.php +++ b/tests/acceptance/TestHelpers/KeycloakHelper.php @@ -46,7 +46,6 @@ class KeycloakHelper { 'offline_access' => 'e2145b30-bf6f-49fb-af3f-1b40168bfcef', ]; private static ?string $adminAccessToken = null; - private static ?int $adminAccessTokenExpiry = null; /** * @return bool @@ -73,7 +72,6 @@ public static function getKeycloakUrl(): string { */ public static function setAdminAccessToken(string $accessToken): void { self::$adminAccessToken = $accessToken; - self::$adminAccessTokenExpiry = self::extractExpiry($accessToken); } /** @@ -83,26 +81,6 @@ public static function setAdminAccessToken(string $accessToken): void { */ public static function resetAdminAccessToken(): void { self::$adminAccessToken = null; - self::$adminAccessTokenExpiry = null; - } - - /** - * Decodes the "exp" claim from a JWT without verifying its signature - this token was just - * issued by Keycloak itself, so it is trusted here purely to know when to refresh it. - * - * @param string $jwt - * - * @return int|null - */ - private static function extractExpiry(string $jwt): ?int { - $segments = \explode('.', $jwt); - if (\count($segments) !== 3) { - return null; - } - $payload = \strtr($segments[1], '-_', '+/'); - $payload .= \str_repeat('=', (4 - \strlen($payload) % 4) % 4); - $decoded = \json_decode((string)\base64_decode($payload), true); - return \is_array($decoded) && isset($decoded['exp']) ? (int)$decoded['exp'] : null; } /** @@ -110,10 +88,7 @@ private static function extractExpiry(string $jwt): ?int { * @throws GuzzleException */ public static function getAdminAccessToken(): string { - // refresh a bit before the actual expiry so a token that's barely valid doesn't get used - // for a request that then takes a few seconds to reach the server - $expiringSoon = self::$adminAccessTokenExpiry !== null && self::$adminAccessTokenExpiry - 10 < \time(); - if (self::$adminAccessToken === null || $expiringSoon) { + if (self::$adminAccessToken === null) { self::setAdminAccessToken(self::generateAdminAccessToken()); } diff --git a/tests/acceptance/TestHelpers/WebUIHelper.php b/tests/acceptance/TestHelpers/WebUIHelper.php index 02f3e3e833f..0a2730f5d59 100644 --- a/tests/acceptance/TestHelpers/WebUIHelper.php +++ b/tests/acceptance/TestHelpers/WebUIHelper.php @@ -65,25 +65,6 @@ public static function setUpUser(string $ocisUrl, string $username, string $pass $page = $context->newPage(); $page->goto($ocisUrl, ['waitUntil' => 'networkidle']); $page->waitForSelector(self::$keycloakHeader, ['timeout' => self::$defaultTimeout]); - // // Right after a proxy/frontend pod restart (e.g. an env-config scenario's rollback), - // // the web app can transiently land on its own /login interstitial instead of - // // auto-redirecting to the OIDC provider, even though the pod already reports - // // healthy - the client-side OIDC discovery call can still hit a connection to - // // Keycloak that isn't fully warm yet. Retry the navigation a few times rather than - // // failing outright on what is usually a few-second timing gap. - // $maxAttempts = 3; - // for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { - // try { - // $page->waitForSelector(self::$keycloakHeader, ['timeout' => self::$defaultTimeout]); - // break; - // } catch (\Exception $e) { - // if ($attempt === $maxAttempts) { - // throw $e; - // } - // \usleep(1000 * 1000); - // $page->goto($ocisUrl, ['waitUntil' => 'networkidle']); - // } - // } $page->locator(self::$usernameInput)->fill($username); $page->locator(self::$passwordInput)->fill($password); $page->locator(self::$loginButton)->click(); @@ -118,31 +99,6 @@ public static function setUpUser(string $ocisUrl, string $username, string $pass $page->waitForSelector(self::$filesView, ['timeout' => self::$defaultTimeout]); return $context->storageState(); } catch (\Exception $e) { - // TEMPORARY DEBUG: on failure, capture what the browser was actually looking at - - // which page/URL it landed on and a snippet of the page content - since the - // exception message alone does not say whether it's stuck on a blank page, an OIDC - // error page, an unexpected Keycloak required-action page, or something else. - try { - $debugUrl = isset($page) ? $page->url() : '(no page)'; - echo "DEBUG login failure for '$username' - current URL: $debugUrl\n"; - if (isset($page)) { - $fullContent = $page->content(); - // prefer centering the snippet on the actual error message the web app - // rendered (e.g. "Something went wrong") over just the raw start, - // since that's where the actual OIDC failure detail/reason should be - $errorPos = \strpos($fullContent, 'Something went wrong'); - if ($errorPos !== false) { - $start = max(0, $errorPos - 200); - $snippet = \substr($fullContent, $start, 6000); - } else { - $bodyPos = \strpos($fullContent, 'getMessage() . "\n"; - } throw new Exception("Login failed for user '$username': " . $e->getMessage(), 0, $e); } finally { $context->close(); diff --git a/tests/config/k8s/setup.sh b/tests/config/k8s/setup.sh index adf1295e5e5..5a95043ea14 100644 --- a/tests/config/k8s/setup.sh +++ b/tests/config/k8s/setup.sh @@ -81,9 +81,6 @@ fi if [[ "$ENABLE_VAULT" == "true" ]]; then sed -i '/vault:/{n;s|false|true|}' $CFG_DIR/values.yaml - # bump from the default "error" level so the CreateHome-for-vault-personal - # and MFA-check Debug/Info log lines (silent at "error") are visible for debugging - sed -i '/^logging:/{n;s|level: error|level: debug|}' $CFG_DIR/values.yaml fi # copy custom values file diff --git a/tests/config/k8s/values.yaml b/tests/config/k8s/values.yaml index 3baf0d30fc6..5b4c69ec660 100644 --- a/tests/config/k8s/values.yaml +++ b/tests/config/k8s/values.yaml @@ -8,7 +8,7 @@ ingress: externalDomain: ocis-server logging: - level: debug + level: error insecure: oidcIdpInsecure: true ocisHttpApiInsecure: true @@ -17,7 +17,7 @@ features: authapp: enabled: false vault: - enabled: true + enabled: false emailNotifications: enabled: false smtp: diff --git a/tests/ociswrapper/ocis/k8s.go b/tests/ociswrapper/ocis/k8s.go index 261b8061f70..635e75a5eee 100644 --- a/tests/ociswrapper/ocis/k8s.go +++ b/tests/ociswrapper/ocis/k8s.go @@ -67,12 +67,8 @@ func K8sUpdateEnv(service string, envMap []string) (bool, string) { // that removes them; kubectl set env is additive and never strips a var it isn't told // about. Mark any such brand-new var for removal now, while we still know it is new, or it // silently survives every future rollback. - log.Println(fmt.Sprintf("[%s] DEBUG requested envMap: %s", service, strings.Join(envMap, ", "))) - log.Println(fmt.Sprintf("[%s] DEBUG baseline before newlyIntroduced check: %s", service, strings.Join(K8sOcisInitEnv[service].Envs, ", "))) newlyIntroduced := diffEnvs(K8sOcisInitEnv[service].Envs, envMap) - log.Println(fmt.Sprintf("[%s] DEBUG newlyIntroduced: %s", service, strings.Join(newlyIntroduced, ", "))) K8sOcisInitEnv[service].Envs = append(K8sOcisInitEnv[service].Envs, newlyIntroduced...) - log.Println(fmt.Sprintf("[%s] DEBUG baseline immediately after append, ptr=%p: %s", service, K8sOcisInitEnv[service], strings.Join(K8sOcisInitEnv[service].Envs, ", "))) envSet, skipWaitForService, err := setServiceEnv(service, envMap, "Failed to set env") if err != nil { @@ -394,7 +390,6 @@ func waitPodDelete(podName string, timeout int) (string, error) { func K8sRollback() (bool, string) { for service, config := range K8sOcisInitEnv { - log.Println(fmt.Sprintf("[%s] DEBUG config ptr=%p at top of rollback loop", service, config)) envs := config.Envs log.Println(fmt.Sprintf("[%s] Rolling envs: %s", service, strings.Join(envs, ", "))) podName, err := getPodName(service) From 243f4bcf762f5d54aadb91fb4f374e752764d2eb Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Mon, 7 Sep 2026 09:40:00 +0545 Subject: [PATCH 14/16] test: regenerate access token on expiry Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 4 +- .../acceptance/TestHelpers/KeycloakHelper.php | 187 +++++++++++------- 2 files changed, 122 insertions(+), 69 deletions(-) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index 246cdd05780..606f72a05ad 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -62,7 +62,7 @@ jobs: - suite: apiAuthApp # - suite: apiAntivirus - suite: apiOcm - - suite: apiCollaboration + # - suite: apiCollaboration - suite: apiVault # - suite: "coreApiAuth,coreApiCapabilities,coreApiFavorites,coreApiMain,coreApiVersions" - suite: "coreApiShareManagementBasicToShares,coreApiShareManagementToShares" @@ -379,7 +379,7 @@ jobs: KC_URL: https://keycloak:8443 PROXY_READYZ_URL: ${{ matrix.suite == 'apiVault' && 'http://localhost:19205/readyz' || '' }} run: make test-acceptance-api - + - name: Run Core ${{ matrix.suite }} tests if: startsWith( matrix.suite, 'core' ) env: diff --git a/tests/acceptance/TestHelpers/KeycloakHelper.php b/tests/acceptance/TestHelpers/KeycloakHelper.php index f9f256064e0..f295aa791e6 100644 --- a/tests/acceptance/TestHelpers/KeycloakHelper.php +++ b/tests/acceptance/TestHelpers/KeycloakHelper.php @@ -46,6 +46,7 @@ class KeycloakHelper { 'offline_access' => 'e2145b30-bf6f-49fb-af3f-1b40168bfcef', ]; private static ?string $adminAccessToken = null; + private static ?int $adminAccessTokenExpiry = null; /** * @return bool @@ -72,6 +73,7 @@ public static function getKeycloakUrl(): string { */ public static function setAdminAccessToken(string $accessToken): void { self::$adminAccessToken = $accessToken; + self::$adminAccessTokenExpiry = self::extractExpiry($accessToken); } /** @@ -81,6 +83,26 @@ public static function setAdminAccessToken(string $accessToken): void { */ public static function resetAdminAccessToken(): void { self::$adminAccessToken = null; + self::$adminAccessTokenExpiry = null; + } + + /** + * Decodes the "exp" claim from a JWT without verifying its signature - this token was just + * issued by Keycloak itself, so it is trusted here purely to know when to refresh it. + * + * @param string $jwt + * + * @return int|null + */ + private static function extractExpiry(string $jwt): ?int { + $segments = \explode('.', $jwt); + if (\count($segments) !== 3) { + return null; + } + $payload = \strtr($segments[1], '-_', '+/'); + $payload .= \str_repeat('=', (4 - \strlen($payload) % 4) % 4); + $decoded = \json_decode((string)\base64_decode($payload), true); + return \is_array($decoded) && isset($decoded['exp']) ? (int)$decoded['exp'] : null; } /** @@ -88,7 +110,14 @@ public static function resetAdminAccessToken(): void { * @throws GuzzleException */ public static function getAdminAccessToken(): string { - if (self::$adminAccessToken === null) { + // refresh a bit before the actual expiry so a token that's barely valid doesn't get used + // for a request that then takes a few seconds to reach the server. This is only a + // first line of defense - the real safety net is the retry-on-401 wrapped around every + // admin API call below, since a scenario that restarts oCIS can take an unpredictable + // amount of time depending on the environment (a single k8s pod's rolling restart vs. a + // full single-binary process restart) + $expiringSoon = self::$adminAccessTokenExpiry !== null && self::$adminAccessTokenExpiry - 10 < \time(); + if (self::$adminAccessToken === null || $expiringSoon) { self::setAdminAccessToken(self::generateAdminAccessToken()); } @@ -103,6 +132,27 @@ private static function getAuthorizationHeader(): array { return [ 'Authorization' => 'Bearer ' . self::getAdminAccessToken() ]; } + /** + * Runs an admin API request, retrying once with a freshly-generated token if the first + * attempt comes back 401. This is the real safety net against a stale cached token - the + * proactive expiry check in getAdminAccessToken() is just a first line of defense, since how + * long a config-change scenario takes (and therefore whether the cached token survives it) + * varies by environment in a way that a fixed margin cannot reliably predict. + * + * @param callable(array): ResponseInterface $sendRequest + * + * @return ResponseInterface + * @throws GuzzleException + */ + private static function sendAdminRequest(callable $sendRequest): ResponseInterface { + $response = $sendRequest(self::getAuthorizationHeader()); + if ($response->getStatusCode() === 401) { + self::resetAdminAccessToken(); + $response = $sendRequest(self::getAuthorizationHeader()); + } + return $response; + } + /** * @param string $roleName * @@ -134,16 +184,16 @@ public static function createUser( ?string $displayName = null, ): ResponseInterface { $url = self::getKeycloakUrl() . '/admin/realms/oCIS/users'; - - return HttpRequestHelper::post( - $url, - null, - null, - array_merge( - self::getAuthorizationHeader(), - [ 'Content-Type' => 'application/json' ], + $body = self::prepareCreateUserPayload($username, $password, $email, $displayName); + + return self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => HttpRequestHelper::post( + $url, + null, + null, + array_merge($authHeader, [ 'Content-Type' => 'application/json' ]), + $body, ), - self::prepareCreateUserPayload($username, $password, $email, $displayName), ); } @@ -166,15 +216,15 @@ public static function assignRole( self::getRealmRole($ocisRole), self::getRealmRole('offline_access'), ]; - return HttpRequestHelper::post( - $url, - null, - null, - array_merge( - self::getAuthorizationHeader(), - [ 'Content-Type' => 'application/json' ], + $encodedBody = json_encode($body, JSON_THROW_ON_ERROR); + return self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => HttpRequestHelper::post( + $url, + null, + null, + array_merge($authHeader, [ 'Content-Type' => 'application/json' ]), + $encodedBody, ), - json_encode($body, JSON_THROW_ON_ERROR), ); } @@ -196,15 +246,15 @@ public static function unassignRole( $body = [ self::getRealmRole($ocisRole), ]; - return HttpRequestHelper::delete( - $url, - null, - null, - array_merge( - self::getAuthorizationHeader(), - [ 'Content-Type' => 'application/json' ], + $encodedBody = json_encode($body, JSON_THROW_ON_ERROR); + return self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => HttpRequestHelper::delete( + $url, + null, + null, + array_merge($authHeader, [ 'Content-Type' => 'application/json' ]), + $encodedBody, ), - json_encode($body, JSON_THROW_ON_ERROR), ); } @@ -399,11 +449,8 @@ public static function setAccessTokenForKeycloakOcisUser(array $user): array { */ public static function getRealm(): array { $url = self::getKeycloakUrl() . '/admin/realms/oCIS'; - $response = HttpRequestHelper::get( - $url, - null, - null, - self::getAuthorizationHeader(), + $response = self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => HttpRequestHelper::get($url, null, null, $authHeader), ); if ($response->getStatusCode() !== 200) { throw new Exception("Failed to get realm roles."); @@ -424,15 +471,15 @@ public static function updateRealmAttribute(string $key, string $value): Respons $attributes = $realm['attributes'] ?? []; $attributes[$key] = $value; $url = self::getKeycloakUrl() . '/admin/realms/oCIS'; - return HttpRequestHelper::put( - $url, - null, - null, - array_merge( - self::getAuthorizationHeader(), - [ 'Content-Type' => 'application/json' ], + $body = json_encode(['attributes' => $attributes], JSON_THROW_ON_ERROR); + return self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => HttpRequestHelper::put( + $url, + null, + null, + array_merge($authHeader, [ 'Content-Type' => 'application/json' ]), + $body, ), - json_encode(['attributes' => $attributes], JSON_THROW_ON_ERROR), ); } @@ -448,15 +495,15 @@ public static function deleteRealmAttribute(string $key): ResponseInterface { $attributes = $realm['attributes'] ?? []; unset($attributes[$key]); $url = self::getKeycloakUrl() . '/admin/realms/oCIS'; - return HttpRequestHelper::put( - $url, - null, - null, - array_merge( - self::getAuthorizationHeader(), - [ 'Content-Type' => 'application/json' ], + $body = json_encode(['attributes' => $attributes], JSON_THROW_ON_ERROR); + return self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => HttpRequestHelper::put( + $url, + null, + null, + array_merge($authHeader, [ 'Content-Type' => 'application/json' ]), + $body, ), - json_encode(['attributes' => $attributes], JSON_THROW_ON_ERROR), ); } @@ -470,11 +517,8 @@ public static function deleteRealmAttribute(string $key): ResponseInterface { */ public static function getUserIdByUsername(string $username): string { $url = self::getKeycloakUrl() . '/admin/realms/oCIS/users?username=' . \urlencode($username) . '&exact=true'; - $response = HttpRequestHelper::get( - $url, - null, - null, - self::getAuthorizationHeader(), + $response = self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => HttpRequestHelper::get($url, null, null, $authHeader), ); if ($response->getStatusCode() !== 200) { throw new Exception("Failed to look up Keycloak user '$username', status: " . $response->getStatusCode()); @@ -499,11 +543,14 @@ public static function getUserIdByUsername(string $username): string { public static function deleteUserTotpCredentials(string $username): void { $uuid = self::getUserIdByUsername($username); $url = self::getKeycloakUrl() . '/admin/realms/oCIS/users/' . $uuid . '/credentials'; - $response = HttpRequestHelper::get( - $url, - null, - null, - self::getAuthorizationHeader(), + $response = self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => + HttpRequestHelper::get( + $url, + null, + null, + $authHeader, + ), ); if ($response->getStatusCode() !== 200) { throw new Exception("Failed to list credentials for Keycloak user '$username'."); @@ -514,11 +561,14 @@ public static function deleteUserTotpCredentials(string $username): void { $deleteUrl = self::getKeycloakUrl() . '/admin/realms/oCIS/users/' . $uuid . '/credentials/' . $credential['id']; - HttpRequestHelper::delete( - $deleteUrl, - null, - null, - self::getAuthorizationHeader(), + self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => + HttpRequestHelper::delete( + $deleteUrl, + null, + null, + $authHeader, + ), ); } } @@ -532,11 +582,14 @@ public static function deleteUserTotpCredentials(string $username): void { */ public static function deleteKeycloakUser(string $uuid): ResponseInterface { $url = self::getKeycloakUrl() . '/admin/realms/oCIS/users/' . $uuid; - return HttpRequestHelper::delete( - $url, - null, - null, - self::getAuthorizationHeader(), + return self::sendAdminRequest( + static fn (array $authHeader): ResponseInterface => + HttpRequestHelper::delete( + $url, + null, + null, + $authHeader, + ), ); } } From d7970391e47429173ebf68f77a76375e4de67f23 Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Mon, 7 Sep 2026 16:51:11 +0545 Subject: [PATCH 15/16] test: pre remove exiting envs Signed-off-by: Prajwol Amatya --- tests/ociswrapper/ocis/k8s.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/ociswrapper/ocis/k8s.go b/tests/ociswrapper/ocis/k8s.go index 635e75a5eee..1ca94e80808 100644 --- a/tests/ociswrapper/ocis/k8s.go +++ b/tests/ociswrapper/ocis/k8s.go @@ -224,8 +224,10 @@ func setServiceEnv(service string, envMap []string, errMsgPrefix string) (bool, // not on kubectl ever reconciling it). Strip any existing entries for every key this call // touches first, in its own invocation, so the actual set below always starts from a clean // (zero-or-one-entry) state instead of leaving kubectl to reconcile a pre-existing - // duplicate on its own. Removing a key that isn't set is a no-op, so this is safe to do - // unconditionally. + // duplicate on its own. This is best-effort: kubectl errors on `KEY-` when the key isn't + // currently set at all (the common case for a var touched here for the first time), which + // is already the clean slate this step is trying to achieve, so that failure is expected + // and must not block the actual set below. removalArgs := []string{} seenKeys := map[string]bool{} for _, env := range envMap { @@ -242,8 +244,7 @@ func setServiceEnv(service string, envMap []string, errMsgPrefix string) (bool, if exitErr, ok := err.(*exec.ExitError); ok { errMsg = strings.TrimSpace(string(exitErr.Stderr)) } - log.Println(fmt.Sprintf("[%s] Failed to pre-remove existing envs before setting them. %s", service, errMsg)) - return false, true, fmt.Errorf("error removing existing env before set") + log.Println(fmt.Sprintf("[%s] Pre-remove before set found nothing to remove (expected when a key is new): %s", service, errMsg)) } } From acc8c4043319a913b77df9eb9c7aaf755d99af4a Mon Sep 17 00:00:00 2001 From: Prajwol Amatya Date: Mon, 7 Sep 2026 17:02:27 +0545 Subject: [PATCH 16/16] test: debug ociswrapper Signed-off-by: Prajwol Amatya --- .github/workflows/k8s.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/k8s.yml b/.github/workflows/k8s.yml index 606f72a05ad..23042afcc59 100644 --- a/.github/workflows/k8s.yml +++ b/.github/workflows/k8s.yml @@ -339,7 +339,7 @@ jobs: --admin-username admin \ --admin-password admin \ --skip-ocis-run \ - -n ocis-server & + -n ocis-server > /tmp/ociswrapper.log 2>&1 & - name: Install Playwright for vault tests if: matrix.suite == 'apiVault' @@ -395,3 +395,9 @@ jobs: WITH_REMOTE_PHP: "false" K8S: ${{ env.K8S }} run: make test-acceptance-api + + - name: Debug ociswrapper + if: always() + run: | + echo "--- ociswrapper: full log ---" + cat /tmp/ociswrapper.log 2>/dev/null || true