diff --git a/.github/workflows/build-push-test-docker-image.yml b/.github/workflows/build-push-test-docker-image.yml new file mode 100644 index 0000000..b623a17 --- /dev/null +++ b/.github/workflows/build-push-test-docker-image.yml @@ -0,0 +1,50 @@ +name: build-push-test-docker-image + +on: + workflow_dispatch: + +jobs: + build-push-test-docker-image: + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set docker labels and tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/openconext/OpenConext-devconf/OpenConext-devconf + flavor: | + latest=false + suffix=-test + tags: | + type=ref,event=tag + type=semver,pattern={{version}} + type=sha + type=raw,suffix=,value=test + + - name: Build and push the TEST image + uses: docker/build-push-action@v5 + with: + context: . + file: stepup/tests/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/stepup-behat.yml b/.github/workflows/stepup-behat.yml new file mode 100644 index 0000000..c1a3528 --- /dev/null +++ b/.github/workflows/stepup-behat.yml @@ -0,0 +1,43 @@ +name: stepup-behat +on: + pull_request: + push: + branches: [ main, feature/*, bugfix/* ] +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + DOCKER_COMPOSE: docker compose -f docker-compose.yml -f docker-compose-behat.yml + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Init environment + run: | + cd stepup + cp .env.test .env + cp gateway/surfnet_yubikey.yaml.dist gateway/surfnet_yubikey.yaml + ${DOCKER_COMPOSE} up -d + - name: Install composer dependencies on the Behat container + run: | + cd stepup + ${DOCKER_COMPOSE} exec -T behat bash -lc "composer install --ignore-platform-reqs -n" + - name: Sleep for 10 seconds + run: sleep 10s + - name: Run Behat tests + run: | + cd stepup + docker exec -t stepup-behat-1 bash -lc "./behat" + - name: Output logs on failure + if: failure() + run: | + cd stepup + ${DOCKER_COMPOSE} logs diff --git a/stepup/.env.test b/stepup/.env.test new file mode 100644 index 0000000..7548b31 --- /dev/null +++ b/stepup/.env.test @@ -0,0 +1,2 @@ +APP_ENV=smoketest +STEPUP_VERSION:test diff --git a/stepup/README.md b/stepup/README.md index 73a0fbc..965edcb 100644 --- a/stepup/README.md +++ b/stepup/README.md @@ -74,6 +74,70 @@ To mount the code in multiple containers: You can add as many services+local code paths that you need. The recommended way is to use absolute paths and the script requires the name of the service and local code path to be separated by a `:`, for each service. +# Accessing the database from your IDE +The Maria DB container exposes her 3306 port to the outside. So you should be able to connect to the database with +your favorite DBA tool. In PHPStorm I was able to connect to the `mariadb` host by using these setting. + +``` +host: localhost +user: root +password: you know the secret ;) +``` + # PHP 8.2 for development The default development container is based on the base image with PHP7.2. You can override this on a per service basis. Uncomment the appropiate line for this in the file ".env" so it uses the PHP8.2 container. An .env.dist is included that you can use to have your own .env. file. .env is in .gitigore so you can make your own changes. +# Functional testing +The stepup application suite comes with a set of Behat (Gherkin) features. These features test the stepup applications +functionally. These tests range from simple smoketests (can we still vet a token), to more bug report driven +functional tests. And everything in between. + +These tests live in this folder: `stepup/tests/behat/features` + +Custom Contexts where created to perform Stepup specific actions. Some details about these contexts can be read about below. + +## Running the tests +1. The tests are automatically triggered on GitHub Actions when building a Pull Request. The action is named: [`stepup-behat`](https://github.com/OpenConext/OpenConext-devconf/actions/workflows/stepup-behat.yml) +2. Run them manually. + +Step two can be achieved by following these actions. + +1: You must instruct the `devconf` environment that you want to run functional tests. +1. Option 1: Copy the `.env.test` to be the `.env` +2. Option 2: Add these two lines to your existing `.env` file + +```shell +APP_ENV=smoketest +STEPUP_VERSION=test +``` + +2: Next you should start the devconf containers in test mode +1. `$ ./start-dev-env.sh` will start the environment using test images for every component. +2. `$ ./start-dev-env.sh selfservice:/path/to/SelfService` to start certain components with local code mounted (useful during development) +3. Choose if you want to run the containers in the back- or foreground. + +3: Once the containers are up and running, you can run the behat tests +1. Open a shell in the `behat` container `$ docker exec -it stepup-behat-1 bash` +2. Run the tests: + 1. `./behat` will run all available behat tests that are not excluded using the `@SKIP` tag + 2. `./behat features/ra.feature` will only run the `ra.feature` found in the features folder + 3. `./behat features/ra.feature:20` will only run the scenario found on line 20 of the `ra.feature` + 4. TODO: `./behat --filter=selfservice` will only run features marked with the `@selfservice` tag + +## Writing tests +Many of the step definitions are coded in our own Contexts. These contexts are divided into five main contexts. +It should be straightforward where to add new definitions. The contexts are not following all the clean code or solid principles. This code is messy, be warned. + +It can be useful during debugging to use the `$this->diePrintingContent();` statement. This outputs the URI of the browser, and the last received html response. As it is hard to step debug the code that is run in a CURL based browser. + +TODO: Mark your tests with at least one of the pre-defined tags: + +`selfservice` +`ra` +`gateway` +`middleware` +`tiqr` +`demogssp` +`webauthn` + +Note that these tags match the `devconf` names given to the different components. diff --git a/stepup/azuremfa/docker-compose.override.yml b/stepup/azuremfa/docker-compose.override.yml index 881a1d0..8215501 100644 --- a/stepup/azuremfa/docker-compose.override.yml +++ b/stepup/azuremfa/docker-compose.override.yml @@ -4,5 +4,5 @@ services: volumes: - ${AZUREMFA_CODE_PATH}:/var/www/html environment: - - APP_ENV=dev + - APP_ENV=${APP_ENV:-dev} - APP_DEBUG=true diff --git a/stepup/dbschema/createdbs.sql b/stepup/dbschema/createdbs.sql index 3e5f258..c747441 100755 --- a/stepup/dbschema/createdbs.sql +++ b/stepup/dbschema/createdbs.sql @@ -2,25 +2,39 @@ CREATE DATABASE IF NOT EXISTS webauthn; CREATE DATABASE IF NOT EXISTS tiqr; CREATE DATABASE IF NOT EXISTS gateway; CREATE DATABASE IF NOT EXISTS middleware; +CREATE DATABASE IF NOT EXISTS webauthn_test; +CREATE DATABASE IF NOT EXISTS tiqr_test; +CREATE DATABASE IF NOT EXISTS gateway_test; +CREATE DATABASE IF NOT EXISTS middleware_test; CREATE USER IF NOT EXISTS 'webauthn_user'@'%' IDENTIFIED BY 'webauthn_secret'; GRANT ALL PRIVILEGES ON webauthn.* TO 'webauthn_user'@'%'; +GRANT ALL PRIVILEGES ON webauthn_test.* TO 'webauthn_user'@'%'; CREATE USER IF NOT EXISTS 'tiqr_user'@'%' IDENTIFIED BY 'tiqr_secret'; GRANT ALL PRIVILEGES ON tiqr.* TO 'tiqr_user'@'%'; +GRANT ALL PRIVILEGES ON tiqr_test.* TO 'tiqr_user'@'%'; CREATE USER IF NOT EXISTS 'gateway_user'@'%' IDENTIFIED BY 'gateway_secret'; GRANT SELECT ON gateway.* TO 'gateway_user'@'%'; +GRANT SELECT ON gateway_test.* TO 'gateway_user'@'%'; CREATE USER IF NOT EXISTS 'middleware_user'@'%' IDENTIFIED BY 'middleware_secret'; GRANT SELECT,INSERT,DELETE,UPDATE ON middleware.* TO 'middleware_user'@'%'; +GRANT SELECT,INSERT,DELETE,UPDATE ON middleware_test.* TO 'middleware_user'@'%'; CREATE USER IF NOT EXISTS 'mw_gateway_user'@'%' IDENTIFIED BY 'mw_gateway_secret'; GRANT SELECT,INSERT,DELETE,UPDATE ON gateway.* TO 'mw_gateway_user'@'%'; +GRANT SELECT,INSERT,DELETE,UPDATE ON middleware.* TO 'middleware_user'@'%'; +GRANT SELECT,INSERT,DELETE,UPDATE ON gateway_test.* TO 'mw_gateway_user'@'%'; +GRANT SELECT,INSERT,DELETE,UPDATE ON middleware_test.* TO 'middleware_user'@'%'; CREATE USER IF NOT EXISTS 'mw_deploy_user'@'%' IDENTIFIED BY 'mw_deploy_secret'; GRANT ALL PRIVILEGES ON gateway.* TO 'mw_deploy_user'@'%' WITH GRANT OPTION; GRANT ALL PRIVILEGES ON middleware.* TO 'mw_deploy_user'@'%' WITH GRANT OPTION; +GRANT ALL PRIVILEGES ON gateway_test.* TO 'mw_deploy_user'@'%' WITH GRANT OPTION; +GRANT ALL PRIVILEGES ON middleware_test.* TO 'mw_deploy_user'@'%' WITH GRANT OPTION; CREATE USER IF NOT EXISTS 'gw_deploy_user'@'%' IDENTIFIED BY 'gw_deploy_secret'; GRANT ALL PRIVILEGES ON gateway.* TO 'gw_deploy_user'@'%'; +GRANT ALL PRIVILEGES ON gateway_test.* TO 'gw_deploy_user'@'%'; diff --git a/stepup/demogssp/docker-compose.override.yml b/stepup/demogssp/docker-compose.override.yml index f8c119a..a2cdd2b 100644 --- a/stepup/demogssp/docker-compose.override.yml +++ b/stepup/demogssp/docker-compose.override.yml @@ -4,5 +4,5 @@ services: volumes: - ${DEMOGSSP_CODE_PATH}:/var/www/html environment: - - APP_ENV=dev + - APP_ENV=${APP_ENV:-dev} - APP_DEBUG=true \ No newline at end of file diff --git a/stepup/docker-compose-behat.yml b/stepup/docker-compose-behat.yml new file mode 100644 index 0000000..438cb60 --- /dev/null +++ b/stepup/docker-compose-behat.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + + behat: + image: ghcr.io/openconext/openconext-devconf/openconext-devconf:test + environment: + - APP_ENV=${APP_ENV:-prod} + networks: + openconextdev: + volumes: + - ${PWD}/:/config + - /var/run/docker.sock:/var/run/docker.sock diff --git a/stepup/docker-compose.yml b/stepup/docker-compose.yml index 117e95c..87401de 100644 --- a/stepup/docker-compose.yml +++ b/stepup/docker-compose.yml @@ -10,15 +10,24 @@ services: openconextdev: aliases: - ra.dev.openconext.local + - ssp.dev.openconext.local + - selfservice.dev.openconext.local - middleware.dev.openconext.local - gateway.dev.openconext.local + - demogssp.dev.openconext.local + - webauthn.dev.openconext.local - tiqr.dev.openconext.local + - mailcatcher.dev.openconext.local hostname: haproxy.docker + + mariadb: image: mariadb:10.6 environment: MYSQL_ROOT_PASSWORD: secret + ports: + - 3306:3306 networks: openconextdev: volumes: @@ -26,13 +35,22 @@ services: - stepup_mariadb:/var/lib/mysql hostname: mariadb.docker + behat: + image: ghcr.io/openconext/openconext-devconf/openconext-devconf:test + environment: + - APP_ENV=${APP_ENV:-prod} + networks: + openconextdev: + volumes: + - ${PWD}/:/config + - /var/run/docker.sock:/var/run/docker.sock + webauthn: - image: ghcr.io/openconext/stepup-webauthn/stepup-webauthn:prod + image: ghcr.io/openconext/stepup-webauthn/stepup-webauthn:${STEPUP_VERSION:-prod} ports: - 8080:8080 environment: - DATABASE_URL: "mysql://webauthn_user:webauthn_secret@mariadb:3306/webauthn" - APP_ENV: prod + - APP_ENV=${APP_ENV:-prod} volumes: - ${PWD}/:/config networks: @@ -51,9 +69,9 @@ services: middleware: - image: ghcr.io/openconext/stepup-middleware/stepup-middleware:prod + image: ghcr.io/openconext/stepup-middleware/stepup-middleware:${STEPUP_VERSION:-prod} environment: - - APP_ENV=prod + - APP_ENV=${APP_ENV:-prod} networks: openconextdev: volumes: @@ -63,9 +81,9 @@ services: hostname: middleware.docker gateway: - image: ghcr.io/openconext/stepup-gateway/stepup-gateway:prod + image: ghcr.io/openconext/stepup-gateway/stepup-gateway:${STEPUP_VERSION:-prod} environment: - - APP_ENV=prod + - APP_ENV=${APP_ENV:-prod} networks: openconextdev: volumes: @@ -77,9 +95,9 @@ services: ra: - image: ghcr.io/openconext/stepup-ra/stepup-ra:prod + image: ghcr.io/openconext/stepup-ra/stepup-ra:${STEPUP_VERSION:-prod} environment: - - APP_ENV=prod + - APP_ENV=${APP_ENV:-prod} networks: openconextdev: volumes: @@ -89,9 +107,9 @@ services: hostname: ra.docker selfservice: - image: ghcr.io/openconext/stepup-selfservice/stepup-selfservice:prod + image: ghcr.io/openconext/stepup-selfservice/stepup-selfservice:${STEPUP_VERSION:-prod} environment: - - APP_ENV=prod + - APP_ENV=${APP_ENV:-prod} networks: openconextdev: volumes: @@ -101,9 +119,9 @@ services: hostname: selfservice.docker demogssp: - image: ghcr.io/openconext/stepup-gssp-example/stepup-gssp-example:prod + image: ghcr.io/openconext/stepup-gssp-example/stepup-gssp-example:${STEPUP_VERSION:-prod} environment: - - APP_ENV=prod + - APP_ENV=${APP_ENV:-prod} networks: openconextdev: volumes: @@ -111,11 +129,11 @@ services: extra_hosts: - "host.docker.internal:host-gateway" hostname: demogssp.docker - + tiqr: image: ghcr.io/openconext/stepup-tiqr/stepup-tiqr:prod environment: - - APP_ENV=prod + - APP_ENV=${APP_ENV:-prod} networks: openconextdev: volumes: @@ -123,11 +141,11 @@ services: extra_hosts: - "host.docker.internal:host-gateway" hostname: tiqr.docker - + azuremfa: - image: ghcr.io/openconext/stepup-azuremfa/stepup-azuremfa:prod + image: ghcr.io/openconext/stepup-azuremfa/stepup-azuremfa:${STEPUP_VERSION:-prod} environment: - - APP_ENV=prod + - APP_ENV=${APP_ENV:-prod} networks: openconextdev: volumes: @@ -135,7 +153,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" hostname: azuremfa.docker - + mailcatcher: image: sj26/mailcatcher:latest ports: diff --git a/stepup/docs/functional-testing.md b/stepup/docs/functional-testing.md new file mode 100644 index 0000000..93953de --- /dev/null +++ b/stepup/docs/functional-testing.md @@ -0,0 +1,42 @@ + +# Get the tests up and running +The stepup directory is mounted in /config. All tests are present in this directory. +All the Stepup containers have a test variant available. These are tagged with :test eg: + +``` +ghcr.io/openconext/stepup-gateway/stepup-gateway:test +``` + +These are automatically started when the correct variable is set in the .env in the directory where docker-compose.yml is located. +An environment file is provided. Copy it into place to use it: +``` +cp .env.test .env +``` + +You can the start the environment in test mode (APP_ENV=smoketest does the magic inside the containers) +A seperate behat container is provided. It is defined in a docker compose override file. Start it like this: +``` +docker compose -f docker-compose.yml -f docker-compose-behat.yml up -d +``` + +You can now use the shell inside the behat container to start the behat tests. + +Enter the container: +``` +docker compose exec -ti behat bash +``` + +Now you need to install behat +``` +composer install --ignore-platform-req=ext-bcmath +``` +And you can now run the tests +``` +vendor/behat/behat/bin/behat +``` +TODO +- The bootstrap process is not working as it should. The renaming of stepup.example.com to dev.openconext.local might be an issue +- Make start-dev-env.sh compatible by adding a commandline option to start the environment in smoketest mode +- Think of a way to do this in GitHub actions +- Make the logging in all containers docker compatible (log to stdout) + diff --git a/stepup/gateway/docker-compose.override.yml b/stepup/gateway/docker-compose.override.yml index bbb74c3..214674e 100644 --- a/stepup/gateway/docker-compose.override.yml +++ b/stepup/gateway/docker-compose.override.yml @@ -4,5 +4,5 @@ services: volumes: - ${GATEWAY_CODE_PATH}:/var/www/html environment: - - APP_ENV=dev + - APP_ENV=${APP_ENV:-dev} - APP_DEBUG=true diff --git a/stepup/middleware/docker-compose.override.yml b/stepup/middleware/docker-compose.override.yml index ffab6dc..52893ed 100644 --- a/stepup/middleware/docker-compose.override.yml +++ b/stepup/middleware/docker-compose.override.yml @@ -4,5 +4,5 @@ services: volumes: - ${MIDDLEWARE_CODE_PATH}:/var/www/html environment: - - APP_ENV=dev + - APP_ENV=${APP_ENV:-dev} - APP_DEBUG=true diff --git a/stepup/middleware/middleware-whitelist.json b/stepup/middleware/middleware-whitelist.json index f8ad2ab..4aad47e 100644 --- a/stepup/middleware/middleware-whitelist.json +++ b/stepup/middleware/middleware-whitelist.json @@ -1,6 +1,5 @@ { "institutions": [ - "stepup.example.com", "dev.openconext.local", "institution-a.example.com", "institution-b.example.com", diff --git a/stepup/ra/docker-compose.override.yml b/stepup/ra/docker-compose.override.yml index 3e0a3fc..862f782 100644 --- a/stepup/ra/docker-compose.override.yml +++ b/stepup/ra/docker-compose.override.yml @@ -4,5 +4,5 @@ services: volumes: - ${RA_CODE_PATH}:/var/www/html environment: - - APP_ENV=dev + - APP_ENV=${APP_ENV:-dev} - APP_DEBUG=true diff --git a/stepup/selfservice/docker-compose.override.yml b/stepup/selfservice/docker-compose.override.yml index 9253726..5d0a2c0 100644 --- a/stepup/selfservice/docker-compose.override.yml +++ b/stepup/selfservice/docker-compose.override.yml @@ -4,5 +4,5 @@ services: volumes: - ${SELFSERVICE_CODE_PATH}:/var/www/html environment: - - APP_ENV=dev + - APP_ENV=${APP_ENV:-dev} - APP_DEBUG=true diff --git a/stepup/start-dev-env.sh b/stepup/start-dev-env.sh index afbe9b7..ba27e99 100755 --- a/stepup/start-dev-env.sh +++ b/stepup/start-dev-env.sh @@ -1,18 +1,77 @@ #!/usr/bin/env bash +# source .env so that we know when to start in test mode +GREEN="\e[1;32m" +ENDCOLOR="\e[0m" +MODE="dev" + +source .env +if [ "${STEPUP_VERSION}" == "test" ]; then + extra_compose_args="-f docker-compose-behat.yml" + MODE="test" + echo -e "${GREEN}Starting in test mode${ENDCOLOR}" +else + extra_compose_args="" +fi # Read the apps and their code paths from the arguments passed to the script -docker_compose_arg=() +docker_compose_args=() + +# Keep a counter of the number of dev-envs to override +number_of_dev_envs=0 for arg in "$@"; do - app=$(echo $arg | cut -d ':' -f 1) - path=$(echo $arg | cut -d ':' -f 2) - echo "export ${app^^}_CODE_PATH=${path}" >> .start-dev-env-vars - docker_compose_args+=("-f ./${app}/docker-compose.override.yml") + app=$(echo $arg | cut -d ':' -f 1) + path=$(echo $arg | cut -d ':' -f 2) + # Test if the specified path(s) exist. If they do not, halt the script and warn + # the user of this mistake + if [ ! -d ${path} ]; then + # Not going to start the env, so clear the env listing + rm .start-dev-env-listing + echo -e "The specified path for app '${app}' is not a directory. \n"; + echo -e "Please review: '${path}'"; + exit 1; + fi + echo "export ${app^^}_CODE_PATH=${path}" >>.start-dev-env-vars + # Keep a listing of all apps that are started in dev mode, for feedback purposes + echo "${app^^}: ${path}" >>.start-dev-env-listing + docker_compose_args+=("-f ./${app}/docker-compose.override.yml") + let number_of_dev_envs=number_of_dev_envs+1 done -# Read the generated env file with the apps and their code paths -source .start-dev-env-vars ; rm .start-dev-env-vars +# Because numbering is off by one, reference the next arg +let number_of_dev_envs=number_of_dev_envs+1 + +# See if there are .start-dev-env-vars +if [ -f .start-dev-env-vars ]; then + # Read the generated env file with the apps and their code paths + source .start-dev-env-vars + rm .start-dev-env-vars +fi + +if [ -f .start-dev-env-listing ]; then + echo -e "${MODE} overrides:\n" + cat .start-dev-env-listing + # Remove the listing + rm .start-dev-env-listing +fi + + +while true; do + read -p "Do you wish to run Docker compose in the foreground? (press ENTER for Yes)" yn + case $yn in + [Nn]* ) + # Use docker compose to start the environment but with the modified override file(s) + echo -e "\nStarting the ${MODE} environment with the following command:\n" + + echo -e "docker compose -f docker-compose.yml "${docker_compose_args[@]}" "${extra_compose_args}" up -d "${@:$number_of_dev_envs}"\n" + docker compose -f docker-compose.yml ${docker_compose_args[@]} ${extra_compose_args} up -d "${@:$number_of_dev_envs}" + break;; + * ) + # Use docker compose to start the environment but with the modified override file(s) + echo -e "Starting the ${MODE} environment with the following command:\n" + + echo -e "docker compose -f docker-compose.yml "${docker_compose_args[@]}" "${extra_compose_args}" up "${@:$number_of_dev_envs}"\n" + docker compose -f docker-compose.yml ${docker_compose_args[@]} ${extra_compose_args} up "${@:$number_of_dev_envs}" + break;; + esac +done -# Use docker compose to start the environment but with the modified override file(s) -echo -e "Starting the dev environment with the following command:\n" -echo -e "docker compose -f docker-compose.yml ${docker_compose_args[@]} up "${@:3}"\n" -docker compose -f docker-compose.yml ${docker_compose_args[@]} up "${@:3}" diff --git a/stepup/tests/Dockerfile b/stepup/tests/Dockerfile new file mode 100644 index 0000000..e4a8ca2 --- /dev/null +++ b/stepup/tests/Dockerfile @@ -0,0 +1,4 @@ +FROM ghcr.io/openconext/openconext-basecontainers/php72-apache2-node16-composer2:latest +RUN apt-get update && \ + apt-get install -y docker.io mariadb-client +WORKDIR /config/tests/behat diff --git a/stepup/tests/behat/.gitignore b/stepup/tests/behat/.gitignore new file mode 100644 index 0000000..49ce3c1 --- /dev/null +++ b/stepup/tests/behat/.gitignore @@ -0,0 +1 @@ +/vendor \ No newline at end of file diff --git a/stepup/tests/behat/.phpactor.json b/stepup/tests/behat/.phpactor.json new file mode 100644 index 0000000..86c7780 --- /dev/null +++ b/stepup/tests/behat/.phpactor.json @@ -0,0 +1,5 @@ +{ + "$schema": "/home/bart/.local/share/nvim/mason/packages/phpactor/phpactor.schema.json", + "prophecy.enabled": false, + "behat.enabled": true +} \ No newline at end of file diff --git a/stepup/tests/behat/behat b/stepup/tests/behat/behat new file mode 100755 index 0000000..b9be949 --- /dev/null +++ b/stepup/tests/behat/behat @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# A convenience script to run the behat tests with the correct config +# For now the only feature is that it pipes on any argument provided +# to the script, into the behat command + +./vendor/bin/behat --config config/behat.yml --tags=~SKIP $1 diff --git a/stepup/tests/behat/composer.json b/stepup/tests/behat/composer.json new file mode 100644 index 0000000..7708b63 --- /dev/null +++ b/stepup/tests/behat/composer.json @@ -0,0 +1,20 @@ +{ + "name": "surfnet/stepup-behat", + "description": "Automated functional tests for Stepup", + "type": "project", + "require": { + "behat/behat": "^3.4", + "behat/mink-goutte-driver": "^1.2", + "behat/mink-extension": "^2.3", + "phpunit/phpunit": "^5.7", + "moontoast/math": "^1.1", + "ramsey/uuid": "~3.4", + "paragonie/random_compat": "v2.0.17" + }, + "autoload": { + "psr-4": { + "Surfnet\\StepupBehat\\": "features/src/" + } + }, + "license": "Apache-2.0" +} diff --git a/stepup/tests/behat/composer.lock b/stepup/tests/behat/composer.lock new file mode 100644 index 0000000..735cf61 --- /dev/null +++ b/stepup/tests/behat/composer.lock @@ -0,0 +1,3573 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "c5603874b5e4e1bbb28d6e8c3fd1274e", + "packages": [ + { + "name": "behat/behat", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Behat.git", + "reference": "e4bce688be0c2029dc1700e46058d86428c63cab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Behat/zipball/e4bce688be0c2029dc1700e46058d86428c63cab", + "reference": "e4bce688be0c2029dc1700e46058d86428c63cab", + "shasum": "" + }, + "require": { + "behat/gherkin": "^4.5.1", + "behat/transliterator": "^1.2", + "container-interop/container-interop": "^1.2", + "ext-mbstring": "*", + "php": ">=5.3.3", + "psr/container": "^1.0", + "symfony/class-loader": "~2.1||~3.0", + "symfony/config": "~2.3||~3.0||~4.0", + "symfony/console": "~2.7.40||^2.8.33||~3.3.15||^3.4.3||^4.0.3", + "symfony/dependency-injection": "~2.1||~3.0||~4.0", + "symfony/event-dispatcher": "~2.1||~3.0||~4.0", + "symfony/translation": "~2.3||~3.0||~4.0", + "symfony/yaml": "~2.1||~3.0||~4.0" + }, + "require-dev": { + "herrera-io/box": "~1.6.1", + "phpunit/phpunit": "^4.8.36|^6.3", + "symfony/process": "~2.5|~3.0|~4.0" + }, + "bin": [ + "bin/behat" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Behat": "src/", + "Behat\\Testwork": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Scenario-oriented BDD framework for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "Agile", + "BDD", + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" + ], + "time": "2018-08-10T18:56:51+00:00" + }, + { + "name": "behat/gherkin", + "version": "v4.5.1", + "source": { + "type": "git", + "url": "https://github.com/Behat/Gherkin.git", + "reference": "74ac03d52c5e23ad8abd5c5cce4ab0e8dc1b530a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/74ac03d52c5e23ad8abd5c5cce4ab0e8dc1b530a", + "reference": "74ac03d52c5e23ad8abd5c5cce4ab0e8dc1b530a", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.5|~5", + "symfony/phpunit-bridge": "~2.7|~3", + "symfony/yaml": "~2.3|~3" + }, + "suggest": { + "symfony/yaml": "If you want to parse features, represented in YAML files" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Gherkin": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Gherkin DSL parser for PHP 5.3", + "homepage": "http://behat.org/", + "keywords": [ + "BDD", + "Behat", + "Cucumber", + "DSL", + "gherkin", + "parser" + ], + "time": "2017-08-30T11:04:43+00:00" + }, + { + "name": "behat/mink", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/e6930b9c74693dff7f4e58577e1b1743399f3ff9", + "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "~2.1|~3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "suggest": { + "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", + "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", + "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", + "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Browser controller/emulator abstraction for PHP", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "testing", + "web" + ], + "time": "2016-03-05T08:26:18+00:00" + }, + { + "name": "behat/mink-browserkit-driver", + "version": "1.3.3", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkBrowserKitDriver.git", + "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/1b9a7ce903cfdaaec5fb32bfdbb26118343662eb", + "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb", + "shasum": "" + }, + "require": { + "behat/mink": "^1.7.1@dev", + "php": ">=5.3.6", + "symfony/browser-kit": "~2.3|~3.0|~4.0", + "symfony/dom-crawler": "~2.3|~3.0|~4.0" + }, + "require-dev": { + "mink/driver-testsuite": "dev-master", + "symfony/http-kernel": "~2.3|~3.0|~4.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Symfony2 BrowserKit driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "Mink", + "Symfony2", + "browser", + "testing" + ], + "time": "2018-05-02T09:25:31+00:00" + }, + { + "name": "behat/mink-extension", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/Behat/MinkExtension.git", + "reference": "80f7849ba53867181b7e412df9210e12fba50177" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/80f7849ba53867181b7e412df9210e12fba50177", + "reference": "80f7849ba53867181b7e412df9210e12fba50177", + "shasum": "" + }, + "require": { + "behat/behat": "^3.0.5", + "behat/mink": "^1.5", + "php": ">=5.3.2", + "symfony/config": "^2.7|^3.0|^4.0" + }, + "require-dev": { + "behat/mink-goutte-driver": "^1.1", + "phpspec/phpspec": "^2.0" + }, + "type": "behat-extension", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\MinkExtension": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + }, + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com" + } + ], + "description": "Mink extension for Behat", + "homepage": "http://extensions.behat.org/mink", + "keywords": [ + "browser", + "gui", + "test", + "web" + ], + "time": "2018-02-06T15:36:30+00:00" + }, + { + "name": "behat/mink-goutte-driver", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkGoutteDriver.git", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkGoutteDriver/zipball/8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "reference": "8b9ad6d2d95bc70b840d15323365f52fcdaea6ca", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6@dev", + "behat/mink-browserkit-driver": "~1.2@dev", + "fabpot/goutte": "~1.0.4|~2.0|~3.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + } + ], + "description": "Goutte driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "browser", + "goutte", + "headless", + "testing" + ], + "time": "2016-03-05T09:04:22+00:00" + }, + { + "name": "behat/transliterator", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c", + "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Transliterator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "time": "2017-04-04T11:38:05+00:00" + }, + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "time": "2017-02-14T19:40:03+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14T21:17:01+00:00" + }, + { + "name": "fabpot/goutte", + "version": "v3.2.3", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Goutte.git", + "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/3f0eaf0a40181359470651f1565b3e07e3dd31b8", + "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": ">=5.5.0", + "symfony/browser-kit": "~2.1|~3.0|~4.0", + "symfony/css-selector": "~2.1|~3.0|~4.0", + "symfony/dom-crawler": "~2.1|~3.0|~4.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.3 || ^4" + }, + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Goutte\\": "Goutte" + }, + "exclude-from-classmap": [ + "Goutte/Tests" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A simple PHP Web Scraper", + "homepage": "https://github.com/FriendsOfPHP/Goutte", + "keywords": [ + "scraper" + ], + "time": "2018-06-29T15:13:57+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.5.8", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.9", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-06-20T22:16:07+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2022-06-20T21:43:03+00:00" + }, + { + "name": "moontoast/math", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/moontoast-math.git", + "reference": "c2792a25df5cad4ff3d760dd37078fc5b6fccc79" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/moontoast-math/zipball/c2792a25df5cad4ff3d760dd37078fc5b6fccc79", + "reference": "c2792a25df5cad4ff3d760dd37078fc5b6fccc79", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "^0.9.0", + "phpunit/phpunit": "^4.7|>=5.0 <5.4", + "satooshi/php-coveralls": "^0.6.1", + "squizlabs/php_codesniffer": "^2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Moontoast\\Math\\": "src/Moontoast/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A mathematics library, providing functionality for large numbers", + "homepage": "https://github.com/ramsey/moontoast-math", + "keywords": [ + "bcmath", + "math" + ], + "time": "2017-02-16T16:54:46+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2017-10-19T19:58:43+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v2.0.17", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d", + "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2018-07-04T16:31:37+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2017-09-11T18:02:19+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2", + "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2017-11-10T14:09:06+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-07-14T14:27:02+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "sebastian/comparator": "^1.1|^2.0|^3.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2018-08-05T17:53:17+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "^1.3", + "phpunit/php-text-template": "^1.2", + "phpunit/php-token-stream": "^1.4.2 || ^2.0", + "sebastian/code-unit-reverse-lookup": "^1.0", + "sebastian/environment": "^1.3.2 || ^2.0", + "sebastian/version": "^1.0 || ^2.0" + }, + "require-dev": { + "ext-xdebug": "^2.1.4", + "phpunit/phpunit": "^5.7" + }, + "suggest": { + "ext-xdebug": "^2.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2017-04-02T07:44:40+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", + "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2017-12-04T08:55:13+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "5.7.27", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", + "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.6.2", + "phpunit/php-code-coverage": "^4.0.4", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "^1.2.4", + "sebastian/diff": "^1.4.3", + "sebastian/environment": "^1.3.4 || ^2.0", + "sebastian/exporter": "~2.0", + "sebastian/global-state": "^1.1", + "sebastian/object-enumerator": "~2.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "^1.0.6|^2.0.1", + "symfony/yaml": "~2.1|~3.0|~4.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2018-02-01T05:50:59+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2 || ^2.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2017-06-30T09:13:00+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/uuid", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/d09ea80159c1929d75b3f9c60504d613aeb4a1e3", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "^1.0|^2.0|9.99.99", + "php": "^5.4 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "codeception/aspect-mock": "^1.0 | ~2.0.0", + "doctrine/annotations": "~1.2.0", + "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ~2.1.0", + "ircmaxell/random-lib": "^1.1", + "jakub-onderka/php-parallel-lint": "^0.9.0", + "mockery/mockery": "^0.9.9", + "moontoast/math": "^1.1", + "php-mock/php-mock-phpunit": "^0.3|^1.1", + "phpunit/phpunit": "^4.7|^5.0|^6.5", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "ext-ctype": "Provides support for PHP Ctype functions", + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + }, + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "time": "2018-07-19T23:38:55+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-01-29T09:50:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-05-22T07:24:03+00:00" + }, + { + "name": "sebastian/environment", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-11-26T07:53:53+00:00" + }, + { + "name": "sebastian/exporter", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-11-19T08:54:04+00:00" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12T03:26:01+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-02-18T15:18:39+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2016-11-19T07:33:16+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "f6668d1a6182d5a8dec65a1c863a4c1d963816c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/f6668d1a6182d5a8dec65a1c863a4c1d963816c0", + "reference": "f6668d1a6182d5a8dec65a1c863a4c1d963816c0", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/dom-crawler": "~2.8|~3.0|~4.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0|~4.0", + "symfony/process": "~2.8|~3.0|~4.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony BrowserKit Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T09:06:28+00:00" + }, + { + "name": "symfony/class-loader", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/class-loader.git", + "reference": "31db283fc86d3143e7ff87e922177b457d909c30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/31db283fc86d3143e7ff87e922177b457d909c30", + "reference": "31db283fc86d3143e7ff87e922177b457d909c30", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "require-dev": { + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/polyfill-apcu": "~1.1" + }, + "suggest": { + "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\ClassLoader\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ClassLoader Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T11:19:56+00:00" + }, + { + "name": "symfony/config", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "7b08223b7f6abd859651c56bcabf900d1627d085" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/7b08223b7f6abd859651c56bcabf900d1627d085", + "reference": "7b08223b7f6abd859651c56bcabf900d1627d085", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/filesystem": "~2.8|~3.0|~4.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3", + "symfony/finder": "<3.3" + }, + "require-dev": { + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/event-dispatcher": "~3.3|~4.0", + "symfony/finder": "~3.3|~4.0", + "symfony/yaml": "~3.0|~4.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T11:19:56+00:00" + }, + { + "name": "symfony/console", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "6b217594552b9323bcdcfc14f8a0ce126e84cd73" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/6b217594552b9323bcdcfc14f8a0ce126e84cd73", + "reference": "6b217594552b9323bcdcfc14f8a0ce126e84cd73", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log-implementation": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T11:19:56+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/edda5a6155000ff8c3a3f85ee5c421af93cca416", + "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T09:06:28+00:00" + }, + { + "name": "symfony/debug", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "c4625e75341e4fb309ce0c049cbf7fb84b8897cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/c4625e75341e4fb309ce0c049cbf7fb84b8897cd", + "reference": "c4625e75341e4fb309ce0c049cbf7fb84b8897cd", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/http-kernel": "~2.8|~3.0|~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2018-08-03T10:42:44+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "09d7df7bf06c1393b6afc85875993cbdbdf897a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/09d7df7bf06c1393b6afc85875993cbdbdf897a0", + "reference": "09d7df7bf06c1393b6afc85875993cbdbdf897a0", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "psr/container": "^1.0" + }, + "conflict": { + "symfony/config": "<3.3.7", + "symfony/finder": "<3.3", + "symfony/proxy-manager-bridge": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "psr/container-implementation": "1.0" + }, + "require-dev": { + "symfony/config": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DependencyInjection Component", + "homepage": "https://symfony.com", + "time": "2018-08-08T11:42:34+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "452bfc854b60134438e3824b159b0d24a5892331" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/452bfc854b60134438e3824b159b0d24a5892331", + "reference": "452bfc854b60134438e3824b159b0d24a5892331", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "~2.8|~3.0|~4.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DomCrawler Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T10:03:52+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb", + "reference": "b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.3|~4.0", + "symfony/expression-language": "~2.8|~3.0|~4.0", + "symfony/stopwatch": "~2.8|~3.0|~4.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T09:06:28+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c", + "reference": "285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2018-08-10T07:29:05+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2", + "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/translation", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "9749930bfc825139aadd2d28461ddbaed6577862" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/9749930bfc825139aadd2d28461ddbaed6577862", + "reference": "9749930bfc825139aadd2d28461ddbaed6577862", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/finder": "~2.8|~3.0|~4.0", + "symfony/intl": "^2.8.18|^3.2.5|~4.0", + "symfony/yaml": "~3.4|~4.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T11:19:56+00:00" + }, + { + "name": "symfony/yaml", + "version": "v3.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "c2f4812ead9f847cb69e90917ca7502e6892d6b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c2f4812ead9f847cb69e90917ca7502e6892d6b8", + "reference": "c2f4812ead9f847cb69e90917ca7502e6892d6b8", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "~3.4|~4.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2018-08-10T07:34:36+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2018-01-29T19:49:41+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/stepup/tests/behat/config/behat.yml b/stepup/tests/behat/config/behat.yml new file mode 100644 index 0000000..cab68af --- /dev/null +++ b/stepup/tests/behat/config/behat.yml @@ -0,0 +1,38 @@ +default: + autoload: + - '%paths.base%/../features/bootstrap/' + formatters: + progress: + paths: false + suites: + default: + filters: + tags: "~@wip&&~@skip" + paths: + - '%paths.base%/../features' + contexts: + - FeatureContext: ~ + - SecondFactorAuthContext: + spTestUrl: 'https://ssp.dev.openconext.local/simplesaml/sp.php' + - SelfServiceContext: + selfServiceUrl: 'https://selfservice.dev.openconext.local' + mailCatcherUrl: 'http://mailcatcher:1080/messages' + - RaContext: + raUrl: 'https://ra.dev.openconext.local' + - ApiFeatureContext: + apiUrl: 'https://middleware.dev.openconext.local' + - Behat\MinkExtension\Context\MinkContext + + extensions: + Behat\MinkExtension: + base_url: ~ + goutte: ~ + sessions: + default: + goutte: + guzzle_parameters: + verify: False + second: + goutte: + guzzle_parameters: + verify: False diff --git a/stepup/tests/behat/features/bootstrap/ApiFeatureContext.php b/stepup/tests/behat/features/bootstrap/ApiFeatureContext.php new file mode 100644 index 0000000..98146c5 --- /dev/null +++ b/stepup/tests/behat/features/bootstrap/ApiFeatureContext.php @@ -0,0 +1,846 @@ +client = new Client( + [ + 'base_uri' => $apiUrl, + ] + ); + } + + /** + * @Given I have the payload + */ + public function iHaveThePayload(PyStringNode $requestPayload) + { + $this->requestPayload = $requestPayload; + } + + /** + * @param string $requestPayload + */ + public function setPayload($requestPayload) + { + $this->requestPayload = $requestPayload; + } + + /** + * @When /^I request "(GET|PUT|POST|DELETE|PATCH) ([^"]*)"$/ + */ + public function iRequest($httpMethod, $resource) + { + $method = strtoupper($httpMethod); + // Construct request + $this->lastRequest = new Request($method, $resource, $this->requestHeaders, $this->requestPayload); + + $options = array(); + if ($this->authUser) { + $options = [ + 'auth' => [$this->authUser, $this->authPassword], + 'verify' => false, + ]; + } + + try { + // Send request + $this->lastResponse = $this->client->send($this->lastRequest, $options); + + } catch (\GuzzleHttp\Exception\BadResponseException $e) { + $response = $e->getResponse(); + + // Sometimes the request will fail, at which point we have + // no response at all. Let Guzzle give an error here, it's + // pretty self-explanatory. + if ($response === null) { + throw $e; + } + + $this->lastResponse = $e->getResponse(); + throw new \Exception('Bad response: '. $this->lastResponse->getBody()->getContents()); + } + } + + /** + * @Given /^I authenticate to the Middleware API$/ + */ + public function iAuthenticateWithPreProgrammedAndSetMandatoryHeaders() + { + $this->authUser = self::$managementUsername; + $this->authPassword = self::$managementPassword; + + $this->requestHeaders['Content-Type'] = 'application/json'; + $this->requestHeaders['Accept'] = 'application/json'; + } + + /** + * @Given /^I authenticate with user "([^"]*)" and password "([^"]*)"$/ + */ + public function iAuthenticateWithEmailAndPassword($email, $password) + { + $this->authUser = $email; + $this->authPassword = $password; + + $this->requestHeaders['Content-Type'] = 'application/json'; + $this->requestHeaders['Accept'] = 'application/json'; + } + + /** + * @Given /^I set the "([^"]*)" header to be "([^"]*)"$/ + */ + public function iSetTheHeaderToBe($headerName, $value) + { + $this->requestHeaders[$headerName] = $value; + } + + /** + * @Given /^the "([^"]*)" header should be "([^"]*)"$/ + */ + public function theHeaderShouldBe($headerName, $expectedHeaderValue) + { + $response = $this->getLastResponse(); + + assertEquals($expectedHeaderValue, (string)$response->getHeader($headerName)); + } + + /** + * @Given /^the "([^"]*)" header should exist$/ + */ + public function theHeaderShouldExist($headerName) + { + $response = $this->getLastResponse(); + + assertTrue($response->hasHeader($headerName)); + } + + /** + * @Then /^the "([^"]*)" property should equal "([^"]*)"$/ + */ + public function thePropertyEquals($property, $expectedValue) + { + $payload = $this->getScopePayload(); + $actualValue = $this->arrayGet($payload, $property); + + assertEquals( + $expectedValue, + $actualValue, + "Asserting the [$property] property in current scope equals [$expectedValue]: ".json_encode($payload) + ); + } + + /** + * If the value contains multiple values, please provide them comma seperated. + * + * If you want to test if an empty array was provided (which is a perfectly valid setting) leave the expectation + * empty + * + * Finally if you want to test if a certain property is null, use the instituteHasAPropertyWhichEqualsNull step + * definition. + * + * @Given /^institute "([^"]*)" has a property "([^"]*)" which equals '([^']*)'$/ + */ + public function instituteHasAPropertyWhichEquals($institute, $property, $expectedValue) + { + $payload = $this->getScopePayload(); + $instituteConfig = $payload[$institute]; + + $actualValue = json_encode($instituteConfig[$property]); + + assertEquals( + $expectedValue, + $actualValue, + sprintf( + "Asserting the [%s] property in current scope equals [%s]. Payload: %s", + $property, + var_export($expectedValue, true), + json_encode($payload, JSON_PRETTY_PRINT) + ) + ); + } + + /** + * @Then /^the api response status code should be (?P\d+)$/ + */ + public function theApiResponseStatusCodeShouldBe($statusCode) + { + $response = $this->getLastResponse(); + + assertEquals( + $statusCode, + $response->getStatusCode(), + sprintf( + 'Expected status code "%s" does not match observed status code "%s"', + $statusCode, + $response->getStatusCode() + ) + ); + } + + /** + * @Then /^scope into the first "([^"]*)" property$/ + */ + public function scopeIntoTheFirstProperty($scope) + { + $this->scope = "{$scope}.0"; + } + + /** + * @Then /^scope into the "([^"]*)" property$/ + */ + public function scopeIntoTheProperty($scope) + { + $this->scope = $scope; + } + + /** + * @Then /^reset scope$/ + */ + public function resetScope() + { + $this->scope = null; + } + + /** + * @Then /^the "([^"]*)" property should contain "([^"]*)"$/ + */ + public function thePropertyShouldContain($property, $expectedValue) + { + $payload = $this->getScopePayload(); + $actualValue = $this->arrayGet($payload, $property); + + // if the property is actually an array, use JSON so we look in it deep + $actualValue = is_array($actualValue) ? json_encode($actualValue, JSON_PRETTY_PRINT) : $actualValue; + assertContains( + $expectedValue, + $actualValue, + "Asserting the [$property] property in current scope contains [$expectedValue]: ".json_encode($payload) + ); + } + + /** + * @Given /^the "([^"]*)" property should not contain "([^"]*)"$/ + */ + public function thePropertyShouldNotContain($property, $expectedValue) + { + $payload = $this->getScopePayload(); + $actualValue = $this->arrayGet($payload, $property); + + // if the property is actually an array, use JSON so we look in it deep + $actualValue = is_array($actualValue) ? json_encode($actualValue, JSON_PRETTY_PRINT) : $actualValue; + assertNotContains( + $expectedValue, + $actualValue, + "Asserting the [$property] property in current scope does not contain [$expectedValue]: ".json_encode( + $payload + ) + ); + } + + /** + * @Then /^the "([^"]*)" property should exist$/ + */ + public function thePropertyExists($property) + { + $payload = $this->getScopePayload(); + + $message = sprintf( + 'Asserting the [%s] property exists in the scope [%s]: %s', + $property, + $this->scope, + json_encode($payload) + ); + + assertTrue($this->arrayHas($payload, $property), $message); + } + + /** + * @Then /^the "([^"]*)" property should not exist$/ + */ + public function thePropertyDoesNotExist($property) + { + $payload = $this->getScopePayload(); + + $message = sprintf( + 'Asserting the [%s] property does not exist in the scope [%s]: %s', + $property, + $this->scope, + json_encode($payload) + ); + + assertFalse($this->arrayHas($payload, $property), $message); + } + + /** + * @Then /^the "([^"]*)" property should be an array$/ + */ + public function thePropertyIsAnArray($property) + { + $payload = $this->getScopePayload(); + + $actualValue = $this->arrayGet($payload, $property); + + assertTrue( + is_array($actualValue), + "Asserting the [$property] property in current scope [{$this->scope}] is an array: ".json_encode($payload) + ); + } + + /** + * @Then /^the "([^"]*)" property should be an object$/ + */ + public function thePropertyIsAnObject($property) + { + $payload = $this->getScopePayload(); + + $actualValue = $this->arrayGet($payload, $property); + + assertTrue( + is_object($actualValue), + "Asserting the [$property] property in current scope [{$this->scope}] is an object: ".json_encode($payload) + ); + } + + /** + * @Then /^the "([^"]*)" property should be an empty array$/ + */ + public function thePropertyIsAnEmptyArray($property) + { + $payload = $this->getScopePayload(); + $scopePayload = $this->arrayGet($payload, $property); + + assertTrue( + is_array($scopePayload) and $scopePayload === array(), + "Asserting the [$property] property in current scope [{$this->scope}] is an empty array: ".json_encode( + $payload + ) + ); + } + + /** + * @Then /^the "([^"]*)" property should contain (\d+) item(?:|s)$/ + */ + public function thePropertyContainsItems($property, $count) + { + $payload = $this->getScopePayload(); + + assertCount( + (int) $count, + $this->arrayGet($payload, $property), + "Asserting the [$property] property contains [$count] items: ".json_encode($payload) + ); + } + + /** + * @Then /^the "([^"]*)" property should be an integer$/ + */ + public function thePropertyIsAnInteger($property) + { + $payload = $this->getScopePayload(); + + isType( + 'int', + $this->arrayGet($payload, $property), + "Asserting the [$property] property in current scope [{$this->scope}] is an integer: ".json_encode($payload) + ); + } + + /** + * @Then /^the "([^"]*)" property should be a string$/ + */ + public function thePropertyIsAString($property) + { + $payload = $this->getScopePayload(); + + isType( + 'string', + $this->arrayGet($payload, $property, true), + "Asserting the [$property] property in current scope [{$this->scope}] is a string: ".json_encode($payload) + ); + } + + /** + * @Then /^the "([^"]*)" property should be a string equalling "([^"]*)"$/ + */ + public function thePropertyIsAStringEqualling($property, $expectedValue) + { + $payload = $this->getScopePayload(); + + $this->thePropertyIsAString($property); + + $actualValue = $this->arrayGet($payload, $property); + + assertSame( + $actualValue, + $expectedValue, + "Asserting the [$property] property in current scope [{$this->scope}] is a string equalling [$expectedValue]." + ); + } + + /** + * @Then /^the "([^"]*)" property should be a boolean$/ + */ + public function thePropertyIsABoolean($property) + { + $payload = $this->getScopePayload(); + + assertTrue( + gettype($this->arrayGet($payload, $property)) == 'boolean', + "Asserting the [$property] property in current scope [{$this->scope}] is a boolean." + ); + } + + /** + * @Then /^the "([^"]*)" property should be a boolean equalling "([^"]*)"$/ + */ + public function thePropertyIsABooleanEqualling($property, $expectedValue) + { + $payload = $this->getScopePayload(); + $actualValue = $this->arrayGet($payload, $property); + + if (!in_array($expectedValue, array('true', 'false'))) { + throw new \InvalidArgumentException("Testing for booleans must be represented by [true] or [false]."); + } + + $this->thePropertyIsABoolean($property); + + assertSame( + $actualValue, + $expectedValue == 'true', + "Asserting the [$property] property in current scope [{$this->scope}] is a boolean equalling [$expectedValue]." + ); + } + + /** + * @Then /^the "([^"]*)" property should be an integer equalling "([^"]*)"$/ + */ + public function thePropertyIsAIntegerEqualling($property, $expectedValue) + { + $payload = $this->getScopePayload(); + $actualValue = $this->arrayGet($payload, $property); + + $this->thePropertyIsAnInteger($property); + + assertSame( + $actualValue, + (int)$expectedValue, + "Asserting the [$property] property in current scope [{$this->scope}] is an integer equalling [$expectedValue]." + ); + } + + /** + * @Then /^the "([^"]*)" property should be either:$/ + */ + public function thePropertyIsEither($property, PyStringNode $options) + { + $payload = $this->getScopePayload(); + $actualValue = $this->arrayGet($payload, $property); + + $valid = explode("\n", (string)$options); + + assertTrue( + in_array($actualValue, $valid), + sprintf( + "Asserting the [%s] property in current scope [{$this->scope}] is in array of valid options [%s].", + $property, + implode(', ', $valid) + ) + ); + } + + /** + * Checks the response exists and returns it. + * + * @throws Exception + * @return ResponseInterface + */ + protected function getLastResponse() + { + if (!$this->lastResponse) { + throw new \Exception("You must first make a request to check a response."); + } + + return $this->lastResponse; + } + + /** + * Return the response payload from the current response. + * + * @throws Exception + */ + protected function getResponsePayload() + { + if (!$this->responsePayload) { + $json = json_decode($this->getLastResponse()->getBody(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $message = 'Failed to decode JSON body '; + + switch (json_last_error()) { + case JSON_ERROR_DEPTH: + $message .= '(Maximum stack depth exceeded).'; + break; + case JSON_ERROR_STATE_MISMATCH: + $message .= '(Underflow or the modes mismatch).'; + break; + case JSON_ERROR_CTRL_CHAR: + $message .= '(Unexpected control character found).'; + break; + case JSON_ERROR_SYNTAX: + $message .= '(Syntax error, malformed JSON): '."\n\n".$this->getLastResponse()->getBody(); + break; + case JSON_ERROR_UTF8: + $message .= '(Malformed UTF-8 characters, possibly incorrectly encoded).'; + break; + default: + $message .= '(Unknown error).'; + break; + } + + throw new Exception($message); + } + + $this->responsePayload = $json; + } + + return $this->responsePayload; + } + + /** + * Returns the payload from the current scope within + * the response. + * + * @return mixed + */ + protected function getScopePayload() + { + $payload = $this->getResponsePayload(); + + if (!$this->scope) { + return $payload; + } + + return $this->arrayGet($payload, $this->scope, true); + } + + /** + * Get an item from an array using "dot" notation. + * + * Adapted further in this project + * + * @copyright Taylor Otwell + * @link http://laravel.com/docs/helpers + * @param array $array + * @param string $key + * @param bool $throwOnMissing + * @param bool $checkForPresenceOnly If true, this function turns into arrayHas + * it just returns true/false if it exists + * @return mixed + * @throws Exception + */ + public static function arrayGet($array, $key, $throwOnMissing = false, $checkForPresenceOnly = false) + { + // this seems like an odd case :/ + if (is_null($key)) { + return $checkForPresenceOnly ? true : $array; + } + + foreach (explode('.', $key) as $segment) { + + if (is_object($array)) { + if (!property_exists($array, $segment)) { + if ($throwOnMissing) { + throw new \Exception(sprintf('Cannot find the key "%s"', $key)); + } + + // if we're checking for presence, return false - does not exist + return $checkForPresenceOnly ? false : null; + } + $array = $array->{$segment}; + + } elseif (is_array($array)) { + if (!array_key_exists($segment, $array)) { + if ($throwOnMissing) { + throw new \Exception(sprintf('Cannot find the key "%s"', $key)); + } + + // if we're checking for presence, return false - does not exist + return $checkForPresenceOnly ? false : null; + } + $array = $array[$segment]; + } + } + + // if we're checking for presence, return true - *does* exist + return $checkForPresenceOnly ? true : $array; + } + + /** + * Same as arrayGet (handles dot.operators), but just returns a boolean + * + * @param $array + * @param $key + * @return boolean + */ + protected function arrayHas($array, $key) + { + return $this->arrayGet($array, $key, false, true); + } + + /** + * @Given /^print last api response$/ + */ + public function printLastResponse() + { + if ($this->lastResponse) { + + // Build the first line of the response (protocol, protocol version, statuscode, reason phrase) + $response = 'HTTP/1.1 '.$this->lastResponse->getStatusCode().' '.$this->lastResponse->getReasonPhrase( + )."\r\n"; + + // Add the headers + foreach ($this->lastResponse->getHeaders() as $key => $value) { + $response .= sprintf("%s: %s\r\n", $key, $value[0]); + } + + // Add the response body + $response .= $this->prettifyJson($this->lastResponse->getBody()); + + // Print the response + $this->printDebug($response); + } + } + + /** + * Returns the prettified equivalent of the input if the input is valid JSON. + * Returns the original input if it is not valid JSON. + * + * @param $input + * + * @return string + * @throws Exception + */ + private function prettifyJson($input) + { + $decodedJson = json_decode($input); + + if ($decodedJson === null) { // JSON is invalid + return $input; + } + + return json_encode($decodedJson, JSON_PRETTY_PRINT); + } + + public function printDebug($string) + { + $this->getOutput()->writeln($string); + } + + /** + * @return ConsoleOutput + */ + private function getOutput() + { + if ($this->output === null) { + $this->output = new ConsoleOutput(); + } + + return $this->output; + } + + /** + * Asserts the the href of the given link name equals this value + * + * Since we're using HAL, this would look for something like: + * "_links.programmer.href": "/api/programmers/Fred" + * + * @Given /^the link "([^"]*)" should exist and its value should be "([^"]*)"$/ + */ + public function theLinkShouldExistAndItsValueShouldBe($linkName, $url) + { + $this->thePropertyEquals( + sprintf('_links.%s.href', $linkName), + $url + ); + } + + /** + * @Given /^the embedded "([^"]*)" should have a "([^"]*)" property equal to "([^"]*)"$/ + */ + public function theEmbeddedShouldHaveAPropertyEqualTo($embeddedName, $property, $value) + { + $this->thePropertyEquals( + sprintf('_embedded.%s.%s', $embeddedName, $property), + $value + ); + } + + /** + * @AfterScenario + */ + public function printLastResponseOnError(AfterScenarioScope $scope) + { + if ($scope->getTestResult()->getResultCode() == TestResult::FAILED) { + if ($this->lastResponse === null) { + return; + } + + $body = $this->lastResponse->getBody()->getContents(); + + $this->printDebug(''); + $this->printDebug('Failure! when making the following request:'); + $this->printDebug( + sprintf( + '%s: %s', + $this->lastRequest->getMethod(), + $this->lastRequest->getUri() + )."\n" + ); + + if (in_array( + $this->lastResponse->getHeader('Content-Type'), + ['application/json', 'application/problem+json'] + )) { + $this->printDebug($this->prettifyJson($body)); + } else { + // the response is HTML - see if we should print all of it or some of it + $isValidHtml = strpos($body, '') !== false; + + if ($this->useFancyExceptionReporting && $isValidHtml) { + $this->printDebug( + 'Failure! Below is a summary of the HTML response from the server.' + ); + + // finds the h1 and h2 tags and prints them only + $crawler = new Crawler($body); + foreach ($crawler->filter('h1, h2')->extract(array('_text')) as $header) { + $this->printDebug(sprintf(' '.$header)); + } + } else { + $this->printDebug($body); + } + } + + } + } +} diff --git a/stepup/tests/behat/features/bootstrap/FeatureContext.php b/stepup/tests/behat/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..bf60f10 --- /dev/null +++ b/stepup/tests/behat/features/bootstrap/FeatureContext.php @@ -0,0 +1,380 @@ +getEnvironment(); + + $this->minkContext = $environment->getContext(MinkContext::class); + $this->apiContext = $environment->getContext(ApiFeatureContext::class); + $this->serlfServiceContext = $environment->getContext(SelfServiceContext::class); + + $this->payloadFactory = new CommandPayloadFactory(); + $this->repository = new SecondFactorRepository(); + $this->institutionConfiguration = new InstitutionConfiguration(); + } + + /** + * @var Identity[] + */ + private $identityStore = []; + + /** + * @Given /^a user "([^"]*)" identified by "([^"]*)" from institution "([^"]*)"$/ + */ + public function aUserIdentifiedByWithAVettedTokenAndTheRole($commonName, $nameId, $institution) + { + $uuid = (string)Uuid::uuid4(); + + return $this->aUserIdentifiedByWithAVettedTokenAndTheRoleWithUuid($commonName, $nameId, $institution, $uuid); + } + + /** + * @Given /^a user "([^"]*)" identified by "([^"]*)" from institution "([^"]*)" and fail with "([^"]*)"$/ + */ + public function anExceptionMessageIsExcpected($commonName, $nameId, $institution, $errorMessage) + { + try { + $uuid = (string)Uuid::uuid4(); + return $this->aUserIdentifiedByWithAVettedTokenAndTheRoleWithUuid($commonName, $nameId, $institution, $uuid); + } catch (Exception $e) { + assertContains($errorMessage, $e->getMessage()); + } + } + + /** + * @Given /^a user "([^"]*)" identified by "([^"]*)" from institution "([^"]*)" with UUID "([^"]*)"$/ + */ + public function aUserIdentifiedByWithAVettedTokenAndTheRoleWithUuid($commonName, $nameId, $institution, $uuid) + { + $userId = (string)$uuid; + + $identity = Identity::from($userId, $nameId, $commonName, $institution, []); + $this->identityStore[$nameId] = $identity; + + $this->setPayload($this->payloadFactory->build('Identity:CreateIdentity', $identity)); + $this->connectToApi('ss', 'secret'); + $this->apiContext->iRequest('POST', '/command'); + } + + /** + * @Given /^institution "([^"]*)" can "([^"]*)" from institution "([^"]*)"$/ + */ + public function theInstitutionIsAuthorizedForAnotherInstitution($institution, $role, $raInstitution) + { + $this->institutionConfiguration->addRole($institution, $role, $raInstitution); + $payload = $this->institutionConfiguration->getPayload(); + + $this->setPayload($payload); + $this->connectToApi('management', 'secret'); + $this->apiContext->iRequest('POST', '/management/institution-configuration'); + } + + private function connectToApi($username, $password) + { + $this->apiContext->iAuthenticateWithEmailAndPassword($username, $password); + } + + private function setPayload($payload) + { + $this->apiContext->setPayload($payload); + } + + /** + * @Given /^the user "([^"]*)" has a vetted "([^"]*)" identified by "([^"]*)"$/ + */ + public function theUserHasAVetted($nameId, $tokenType, $identifier) + { + $this->theUserHasAVettedWithIdentifier($nameId, $tokenType, $identifier); + } + + /** + * @Given /^the user "([^"]*)" has a vetted "([^"]*)" with identifier "([^"]*)"$/ + */ + public function theUserHasAVettedWithIdentifier($nameId, $tokenType, $identifier) + { + // First test if this identity was already provisioned + if (!isset($this->identityStore[$nameId])) { + throw new InvalidArgumentException( + sprintf( + 'This identity "%s" is not yet known use the "aUserIdentifiedByWithAVettedTokenAndTheRole" step to create a new identity.', + $nameId + ) + ); + } + + $tokenId = (string)Uuid::uuid4(); + $token = SecondFactorToken::from($tokenId, $tokenType, $identifier); + $identityData = $this->identityStore[$nameId]; + $identityData->tokens = [$token]; + switch ($tokenType) { + case "yubikey": + // 1: Prove possession of the token + $this->proveYubikeyPossession($identityData); + break; + case "sms": + // 1: Prove possession of the token + $this->proveSmsPosession($identityData); + break; + case "demo-gssp": + // 1: Prove possession of the token + $this->proveGsspPossession($identityData); + break; + default: + throw new InvalidArgumentException("This token type is not yet supported"); + break; + } + // 2: Mail verification + $this->mailVerification($tokenId, $identityData); + + switch ($tokenType) { + case "yubikey": + // 3 Vet the yubikey + $this->vetYubikeyToken($identityData); + break; + case "sms": + // 3 Vet the yubikey + $this->vetSmsToken($identityData); + break; + case "demo-gssp": + // 3 Vet the demogssp token + $this->vetGsspToken($identityData); + break; + default: + throw new InvalidArgumentException("This token type is not yet supported for vetting"); + break; + } + + } + + /** + * @Given /^the user "([^"]*)" has a verified "([^"]*)" with registration code "([^"]*)"$/ + */ + public function theUserHasAVerified($nameId, $tokenType, $registrationCode) + { + // First test if this identity was already provisioned + if (!isset($this->identityStore[$nameId])) { + throw new InvalidArgumentException( + sprintf( + 'This identity "%s" is not yet known use the "aUserIdentifiedByWithAVettedTokenAndTheRole" step to create a new identity.', + $nameId + ) + ); + } + + $tokenId = (string)Uuid::uuid4(); + $this->theUserHasAVerifiedToken($nameId, $tokenType, $registrationCode, $tokenId); + } + + + /** + * @Given /^the user "([^"]*)" has a verified "([^"]*)" with registration code "([^"]*)" and secondFactorId "([^"]*)"$/ + */ + public function theUserHasAVerifiedToken($nameId, $tokenType, $registrationCode, $tokenId) + { + // First test if this identity was already provisioned + if (!isset($this->identityStore[$nameId])) { + throw new InvalidArgumentException( + sprintf( + 'This identity "%s" is not yet known use the "aUserIdentifiedByWithAVettedTokenAndTheRole" step to create a new identity.', + $nameId + ) + ); + } + + $token = SecondFactorToken::from($tokenId, $tokenType, '03945859'); + $identityData = $this->identityStore[$nameId]; + $identityData->tokens = [$token]; + + // 1: Prove possession of the token + $this->proveYubikeyPossession($identityData); + + // 2: Mail verification + $this->mailVerification($tokenId, $identityData); + + // 3. Update the registration code (in the projection..) + $this->repository->updateRegistrationCode($identityData->identityId, $registrationCode); + } + + /** + * @Given /^the user "([^"]*)" has the role "([^"]*)" for institution "([^"]*)"$/ + */ + public function theUserHasTheRole($nameId, $role, $institution) + { + // First test if this identity was already provisioned + if (!isset($this->identityStore[$nameId])) { + throw new InvalidArgumentException( + sprintf( + 'This identity "%s" is not yet known use the "aUserIdentifiedByWithAVettedTokenAndTheRole" step to create a new identity.', + $nameId + ) + ); + } + // TODO: Improve this. At this moment this is the hardcoded actor Id (identity_id) of the admin + $actorId = 'e9ab38c3-84a8-47e6-b371-4da5c303669a'; + $identityData = $this->identityStore[$nameId]; + $payload = $this->payloadFactory->buildRolePayload($actorId, $identityData->identityId, $identityData->institution, $role, $institution); + $this->setPayload($payload); + $this->connectToApi('ra', 'secret'); + $this->apiContext->iRequest('POST', '/command'); + } + + private function proveYubikeyPossession($identityData) + { + // 1.1 prove possession of a yubikey token + $payload = $this->payloadFactory->build('Identity:ProveYubikeyPossession', $identityData); + $this->setPayload($payload); + $this->connectToApi('ss', 'secret'); + $this->apiContext->iRequest('POST', '/command'); + } + + private function proveGsspPossession($identityData) + { + // 1.1 prove possession of a yubikey token + $payload = $this->payloadFactory->build('Identity:ProveGssfPossession', $identityData); + $this->setPayload($payload); + $this->connectToApi('ss', 'secret'); + $this->apiContext->iRequest('POST', '/command'); + } + + private function proveSmsPosession($identityData) + { + // 1.1 prove possession of a yubikey token + $payload = $this->payloadFactory->build('Identity:ProvePhonePossession', $identityData); + $this->setPayload($payload); + $this->connectToApi('ss', 'secret'); + $this->apiContext->iRequest('POST', '/command'); + } + + private function mailVerification($tokenId, $identityData) + { + // 2.1: Mail verification -> get verification nonce + $nonce = $this->repository->findNonceById($tokenId); + $identityData->tokens[0]->nonce = $nonce; + + // 2.2 Verify email was received + $payload = $this->payloadFactory->build("Identity:VerifyEmail", $identityData); + $this->setPayload($payload); + $this->connectToApi('ss', 'secret'); + $this->apiContext->iRequest('POST', '/command'); + } + + private function vetYubikeyToken($identityData) + { + // 3.1. Retrieve the registration code + $activationContext = new ActivationContext(); + $activationContext->registrationCode = $this->repository->getRegistrationCodeByIdentity($identityData->identityId); + + // TODO: Improve this. At this moment this is the hardcoded actor Id (identity_id) of the admin + $activationContext->actorId = 'e9ab38c3-84a8-47e6-b371-4da5c303669a'; + + // 3.2 Vet the second factor device + $identityData->activationContext = $activationContext; + $payload = $this->payloadFactory->build('Identity:VetSecondFactor', $identityData); + $this->setPayload($payload); + $this->connectToApi('ra', 'secret'); + + $this->apiContext->iRequest('POST', '/command'); + + } + + private function vetSmsToken($identityData) + { + // 3.1. Retrieve the registration code + $activationContext = new ActivationContext(); + $activationContext->registrationCode = $this->repository->getRegistrationCodeByIdentity($identityData->identityId); + // TODO: Improve this. At this moment this is the hardcoded actor Id (identity_id) of the admin + $activationContext->actorId = 'e9ab38c3-84a8-47e6-b371-4da5c303669a'; + + // 3.2 Vet the second factor device + $identityData->activationContext = $activationContext; + $payload = $this->payloadFactory->build('Identity:VetSecondFactor', $identityData); + + $this->setPayload($payload); + $this->connectToApi('ra', 'secret'); + $this->apiContext->iRequest('POST', '/command'); + } + + private function vetGsspToken($identityData) + { + // 3.1. Retrieve the registration code + $activationContext = new ActivationContext(); + $activationContext->registrationCode = $this->repository->getRegistrationCodeByIdentity($identityData->identityId); + // TODO: Improve this. At this moment this is the hardcoded actor Id (identity_id) of the admin + $activationContext->actorId = 'e9ab38c3-84a8-47e6-b371-4da5c303669a'; + + // 3.2 Vet the second factor device + $identityData->activationContext = $activationContext; + $payload = $this->payloadFactory->build('Identity:VetSecondFactor', $identityData); + + $this->setPayload($payload); + $this->connectToApi('ra', 'secret'); + $this->apiContext->iRequest('POST', '/command'); + } +} diff --git a/stepup/tests/behat/features/bootstrap/RaContext.php b/stepup/tests/behat/features/bootstrap/RaContext.php new file mode 100644 index 0000000..8b42cc4 --- /dev/null +++ b/stepup/tests/behat/features/bootstrap/RaContext.php @@ -0,0 +1,708 @@ +raUrl = $raUrl; + } + + /** + * @BeforeScenario + */ + public function gatherContexts(BeforeScenarioScope $scope) + { + $environment = $scope->getEnvironment(); + + $this->minkContext = $environment->getContext(MinkContext::class); + $this->authContext = $environment->getContext(SecondFactorAuthContext::class); + $this->selfServiceContext = $environment->getContext(SelfServiceContext::class); + } + + /** + * @Given I vet my :tokenType second factor at the information desk + */ + public function iVetMySecondFactorAtTheInformationDesk(string $tokenType) + { + // We visit the RA location url + $this->minkContext->visit($this->raUrl); + $this->iAmLoggedInIntoTheRaPortalAs('admin', 'yubikey'); + + $this->iVetASpecificSecondFactor( + $tokenType, + $this->selfServiceContext->getVerifiedSecondFactorId(), + $this->selfServiceContext->getActivationCode() + ); + } + + /** + * @Given /^I vet the last added second factor$/ + */ + public function iVetTheLastAddedSecondFactor() + { + $secondFactorId = $this->selfServiceContext->getVerifiedSecondFactorId(); + $activationCode = $this->selfServiceContext->getActivationCode(); + + $this->findsTokenForActivation($activationCode); + $this->userProvesPosessionOfSmsToken($secondFactorId); + $this->adminVerifiesUserIdentity($secondFactorId); + $this->vettingProcessIsCompleted($secondFactorId); + } + + public function iVetASpecificSecondFactor($tokenType, $secondFactorId, $activationCode) + { + // We visit the RA location url + $this->minkContext->visit($this->raUrl); + + $this->findsTokenForActivation($activationCode); + switch ($tokenType) { + case "Yubikey": + $this->userProvesPosessionOfYubikeyToken($secondFactorId); + break; + case "SMS": + $this->userProvesPosessionOfSmsToken($secondFactorId); + break; + case "Demo GSSP": + $this->userProvesPosessionOfDemoGsspToken($secondFactorId); + break; + default: + throw new Exception(sprintf('The %s token type is not supported', $tokenType)); + } + $this->adminVerifiesUserIdentity($secondFactorId); + $this->vettingProcessIsCompleted($secondFactorId); + } + + /** + * @Given /^I vet a second factor with id "([^"]*)" and with activation code "([^"]*)"$/ + */ + public function iVetASecondFactor($secondFactorId, $activationCode) + { + // We visit the RA location url + $this->minkContext->visit($this->raUrl); + + $this->findsTokenForActivation($activationCode); + $this->userProvesPosessionOfSmsToken($secondFactorId); + $this->adminVerifiesUserIdentity($secondFactorId); + $this->vettingProcessIsCompleted($secondFactorId); + } + + + /** + * @Given /^I am logged in into the ra portal as "([^"]*)" with a "([^"]*)" token$/ + */ + public function iAmLoggedInIntoTheRaPortalAs($userName, $tokenType) + { + // Login into RA + $this->iTryToLoginIntoTheRaPortalAs($userName, $tokenType); + // We are now on the RA homepage + $this->minkContext->assertPageAddress('https://ra.dev.openconext.local'); + $this->iSwitchLocaleTo('English'); + $this->minkContext->assertPageContainsText('RA Management Portal'); + $this->minkContext->assertPageContainsText('Token activation'); + } + + /** + * @Given /^I try to login into the ra portal as "([^"]*)" with a "([^"]*)" token$/ + */ + public function iTryToLoginIntoTheRaPortalAs($userName, $tokenType) + { + // We visit the RA location url + $this->minkContext->getSession()->reset(); + $this->minkContext->visit($this->raUrl); + + // The admin user logs in and gives a Yubikey second factor + $this->authContext->authenticateWithIdentityProviderForWithStepup($userName); + switch ($tokenType) { + case "yubikey": + $this->authContext->verifyYuikeySecondFactor(); + break; + case "demo-gssp": + $this->authContext->verifyGsspSecondFactor(); + break; + default: + throw new Exception( + sprintf( + 'Second factor type of "%s" is not yet supported in the tests.', + $tokenType + ) + ); + } + } + + + /** + * @Given /^I visit the "([^"]*)" page in the RA environment$/ + */ + public function iVisitAPageInTheRaEnvironment($uri) + { + // We visit the RA location url + $this->minkContext->visit($this->raUrl.'/'.$uri); + } + + public function findsTokenForActivation($activationCode) + { + // The activation token was previously set on the SP context, and can be retrieved here. + $this->minkContext->fillField('ra_start_vetting_procedure_registrationCode', $activationCode); + $this->minkContext->pressButton('Search'); + } + + /** + * @When /^I search for "([^"]*)" on the token activation page$/ + */ + public function iSearchForOnTheTokenActivationPage($registrationCode) + { + // We visit the RA location url which is the search page + $this->minkContext->visit($this->raUrl); + $this->minkContext->fillField('ra_start_vetting_procedure_registrationCode', $registrationCode); + $this->minkContext->pressButton('Search'); + } + + private function userProvesPosessionOfDemoGsspToken(string $secondFactorId) + { + $vettingProcedureUrl = 'https://ra.dev.openconext.local/vetting-procedure/%s/gssf/demo_gssp/initiate-verification'; + + $this->minkContext->assertPageAddress( + sprintf( + $vettingProcedureUrl, + $secondFactorId + ) + ); + // Press the initiate vetting procedure button in the search results + $this->minkContext->pressButton('ra_initiate_gssf_submit'); + + // Ensure we have the english translation + $this->minkContext->clickLink('EN'); + + // Press the Authenticate button on the Demo authentication page + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/authentication'); + $this->minkContext->pressButton('button_authenticate'); + // Pass through the Demo Gssp redirection page. + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/saml/sso_return'); + $this->minkContext->pressButton('Submit'); + // Pass through the 'return to sp' redirection page. + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/gssp/demo_gssp/consume-assertion'); + $this->minkContext->pressButton('Submit'); + } + + private function userProvesPosessionOfSmsToken($secondFactorId) + { + $vettingProcedureUrl = 'https://ra.dev.openconext.local/vetting-procedure/%s/send-sms-challenge'; + + $this->minkContext->assertPageAddress( + sprintf( + $vettingProcedureUrl, + $secondFactorId + ) + ); + + // Press the initiate vetting procedure button in the search results + $this->minkContext->pressButton('Send code'); + // Fill the Code field with an arbitrary verification code + $this->minkContext->fillField('ra_verify_phone_number_challenge', '999'); + $this->minkContext->pressButton('Verify code'); + } + + private function userProvesPosessionOfYubikeyToken($secondFactorId) + { + $vettingProcedureUrl = 'https://ra.dev.openconext.local/vetting-procedure/%s/verify-yubikey'; + + $this->minkContext->assertPageAddress( + sprintf( + $vettingProcedureUrl, + $secondFactorId + ) + ); + // Fill the Code field with an arbitrary verification code + $this->minkContext->fillField('ra_verify_yubikey_public_id_otp', '999'); + $page = $this->minkContext->getSession()->getPage(); + $form = $page->find('css', 'form[name="ra_verify_yubikey_public_id"]'); + $form->submit(); + } + + private function adminVerifiesUserIdentity($verifiedSecondFactorId) + { + $vettingProcedureUrl = 'https://ra.dev.openconext.local/vetting-procedure/%s/verify-identity'; + + $this->minkContext->assertPageAddress( + sprintf( + $vettingProcedureUrl, + $verifiedSecondFactorId + ) + ); + $this->iVerifyTheUsersIdentityByFillingTheLastCharactersOfTheDocumentNumber(); + } + + private function vettingProcessIsCompleted($verifiedSecondFactorId) + { + $vettingProcedureUrl = 'https://ra.dev.openconext.local/vetting-procedure/%s/completed'; + + $this->minkContext->assertPageAddress( + sprintf( + $vettingProcedureUrl, + $verifiedSecondFactorId + ) + ); + $this->minkContext->assertPageContainsText('Token activated'); + $this->minkContext->assertPageContainsText('The user has proven posession of his token'); + } + + /** + * @When /^I switch to institution "([^"]*)" with institution switcher$/ + */ + public function iSwitchWithRaaSwitcherToInstitutionWithName($institutionName) + { + $this->minkContext->selectOption('select_institution_institution', $institutionName); + $this->minkContext->pressButton('select_institution_select_and_apply'); + } + + /** + * @Given /^I visit the RA promotion page$/ + */ + public function iVisitTheRAManagementRAPromotionPage() + { + $this->minkContext->assertElementOnPage('[href="/management/ra"]'); + $this->minkContext->clickLink('RA Management'); + $this->minkContext->assertElementOnPage('[href="/management/search-ra-candidate"]'); + $this->minkContext->clickLink('Add RA(A)'); + $this->minkContext->assertPageAddress('https://ra.dev.openconext.local/management/search-ra-candidate'); + } + + /** + * @Given /^I visit the Tokens page$/ + */ + public function iVisitTheTokensPage() + { + $this->minkContext->clickLink('Tokens'); + $this->minkContext->assertPageAddress('https://ra.dev.openconext.local/second-factors'); + } + + /** + * @When I filter the :arg1 filter on :arg2 + */ + public function iFilterTheForm($filter, $filterValue) + { + $this->minkContext->assertPageAddress('https://ra.dev.openconext.local/second-factors'); + $this->minkContext->selectOption($filter, $filterValue); + $this->minkContext->pressButton('Search'); + } + + /** + * @Then I should see :arg1 in the search results + */ + public function searchResultsShouldInclude($expectation) + { + $this->minkContext->assertElementContainsText('.search-second-factors table', $expectation); + } + + /** + * @When I open the audit log for a user of :arg1 + */ + public function openFirstAuditLogForInstitution($institution) + { + $page = $this->minkContext->getSession()->getPage(); + $searchResult = $page->find('xpath', sprintf("//td[contains(.,'%s')]/..", $institution)); + if (is_null($searchResult) || !$searchResult->has('css', 'a.audit-log')) { + throw new Exception( + sprintf('No tokens found for institution "%s"', $institution) + ); + } + $searchResult->clickLink('Audit log'); + } + + /** + * @When I click on the token export button + */ + public function clickOnTheTokenExportButton() + { + $page = $this->minkContext->getSession()->getPage(); + $page->pressButton('Export'); + } + + /** + * @Then I should not see a token export button + */ + public function shouldNotSeeATokenExportButton() + { + $page = $this->minkContext->getSession()->getPage(); + if ($page->hasButton('Export') ) { + throw new Exception( + sprintf('A token export button should not be visible') + ); + } + } + + /** + * @Then I should see :arg1 in the audit log identity overview + */ + public function iShouldSeeInAuditLogIdentityOverview($institution) + { + $page = $this->minkContext->getSession()->getPage(); + $searchResult = $page->find('xpath', sprintf("//td[contains(.,'%s')]", $institution)); + + if (is_null($searchResult)) { + throw new Exception( + sprintf('The institution "%s" was not found in the audit log identity overview.', $institution) + ); + } + } + + /** + * @Given I remove token from user :arg1 + */ + public function removeTokenFromUser($name) + { + $this->removeTokenWithIdentifierFromUser('03945859', $name); + } + + /** + * @Then I remove token with identifier :arg2 from user :arg1 + */ + public function removeTokenWithIdentifierFromUser($identifier, $name) + { + $page = $this->minkContext->getSession()->getPage(); + + // Get row + /** @var NodeElement[] $searchResults */ + $searchResults = $page->findAll('xpath', sprintf("//td[contains(.,'%s')]/..", $name)); + if (empty($searchResults)) { + throw new Exception( + sprintf('No tokens found for user "%s"', $name) + ); + } + $token = null; + foreach ($searchResults as $searchResult) { + $button = $searchResult->find('css', sprintf('[data-sfidentifier=%s].btn.revoke', json_encode($identifier))); + if ($button) { + $token = $button; + } + } + if (is_null($token)) { + throw new Exception( + sprintf('Token with identifier not found for user "%s"', $name) + ); + } + + // Get button data + $sfIdentifier = $token->getAttribute('data-sfid'); + $identityIdentifier = $token->getAttribute('data-sfidentityid'); + + // Fill data in hidden revocation form + $page->find('css', 'input[name="ra_revoke_second_factor[identityId]"]')->setValue($identityIdentifier); + $page->find('css', 'input[name="ra_revoke_second_factor[secondFactorId]"]')->setValue($sfIdentifier); + $form = $page->find('css', "form[name=ra_revoke_second_factor]"); + $form->submit(); + + $this->minkContext->assertElementContainsText('#flash .alert', 'The token has been successfully removed'); + } + + + /** + * @Then I should not see :arg1 in the search results + */ + public function searchResultsShouldNotInclude($expectation) + { + $this->minkContext->assertElementNotContainsText('.search-second-factors table', $expectation); + } + + /** + * @Then /^I change the role of "([^"]*)" to become "([^"]*)" for institution "([^"]*)"$/ + */ + public function iChangeTheRoleOfToBecome($userName, $role, $institution) + { + if (!in_array($role, ['RA', 'RAA']) ) { + throw new Exception( + sprintf('The role %s is invalid', $role) + ); + } + + $this->minkContext->assertPageAddress('https://ra.dev.openconext.local/management/search-ra-candidate'); + $this->minkContext->fillField('ra_search_ra_candidates_name', $userName); + $this->minkContext->pressButton('ra_search_ra_candidates_search'); + $page = $this->minkContext->getSession()->getPage(); + + // There should be a td with the username in it, select that TR to press that button on. + $searchResult = $page->find('xpath', sprintf("//td[contains(.,'%s')]/..", $userName)); + + if (is_null($searchResult) || !$searchResult->has('css', 'a.btn-info[role="button"]')) { + throw new Exception( + sprintf('The user with username "%s" could not be found in the search results', $userName) + ); + } + + $searchResult->pressButton('Add role'); + + $this->minkContext->assertPageContainsText('Contact Information'); + $this->minkContext->assertPageContainsText($userName); + + // Fill the form with arbitrary text + $this->minkContext->fillField('ra_management_create_ra_location', 'Basement of institution-a'); + $this->minkContext->fillField('ra_management_create_ra_contactInformation', 'Desk B12, Institution A'); + $this->minkContext->selectOption('ra_management_create_ra_roleAtInstitution_role', $role); + $this->minkContext->selectOption('ra_management_create_ra_roleAtInstitution_institution', $institution); + + // Promote the user by clicking the button + $this->minkContext->pressButton('ra_management_create_ra_button-group_create_ra'); + + // If your Session supports Javascript, then enable these two lines. The configured sessions use Goutte, which + // is based on Guzzle and is not able to evaluate Javascript. + + // $this->minkContext->assertPageContainsText('Are you sure you want to give the user below the selected role?'); + // $this->minkContext->pressButton('Confirm'); + + $this->minkContext->assertPageAddress('https://ra.dev.openconext.local/management/search-ra-candidate'); + $this->minkContext->assertPageContainsText('The role of this user has been changed.'); + } + + /** + * @Given /^I visit the RA Management page$/ + */ + public function iVisitTheRAManagementPage() + { + $this->minkContext->assertElementOnPage('[href="/management/ra"]'); + $this->minkContext->clickLink('RA Management'); + $this->minkContext->assertPageContainsText('Add RA(A)'); + } + + /** + * @Then /^I relieve "([^"]*)" from "([^"]*)" of his "([^"]*)" role$/ + */ + public function iRelieveOfHisRole($userName, $institution, $role) + { + $page = $this->minkContext->getSession()->getPage(); + // There should be a td with the username in it, select that TR to press that button on. + $searchResult = $page->findAll('xpath', sprintf("//tr[./td[contains(.,'%s')]]", $userName)); + + /** @var \Behat\Mink\Element\NodeElement $result */ + foreach ($searchResult as $result) { + $raa = $result->find('css', 'td:nth-of-type(4)'); + + if ($raa->getText() === $role.' @ '.$institution) { + + $result->pressButton('Remove role'); + $this->minkContext->assertPageContainsText('Are you sure you want to remove the user below as RA(A)?'); + $this->minkContext->pressButton('Confirm'); + $this->minkContext->assertPageContainsText('The Identity is no longer RA(A)'); + + return; + } + } + + throw new Exception( + sprintf('The ra(a) with username "%s" could not be found in the search results', $userName) + ); + } + + /** + * @Given /^I should see the following candidates:$/ + * @param TableNode $table + */ + public function iShouldSeeTheFollowingCandidates(TableNode $table) + { + $this->iShouldSeeTheFollowingCandidateFors(null, $table); + } + + /** + * @Given /^I should see the following candidates for "([^"]*)":$/ + * @param TableNode $table + */ + public function iShouldSeeTheFollowingCandidateFors($forInstitution, TableNode $table) + { + $page = $this->minkContext->getSession()->getPage(); + + // build hashmap to check identities + $data = []; + $hash = $table->getHash(); + foreach ($hash as $row) { + $key = $row['name'] . '|' . $row['institution']; + $data[$key] = true; + } + + // get identities form page + $searchResult = $page->findAll('xpath', "//tr[./td]"); + foreach ($searchResult as $result) { + + $name = $result->find('css', 'td:nth-of-type(2)')->getText(); + $institution = $result->find('css', 'td:nth-of-type(1)')->getText(); + $key = $name . '|' . $institution; + + if (($forInstitution == $institution || is_null($forInstitution)) && !array_key_exists($key, $data)) { + throw new Exception(sprintf('Unexpected user found on page: "%s"', $key)); + } + + unset($data[$key]); + } + + // check if all are found + if (!empty($data)) { + throw new Exception(sprintf('User(s) not found on page: "%s"', json_encode(array_keys($data)))); + } + } + + /** + * @Given /^I should see the following raas:$/ + * @param TableNode $table + */ + public function iShouldSeeTheFollowingRaas(TableNode $table) + { + $page = $this->minkContext->getSession()->getPage(); + + // build hashmap to check identities + $data = []; + $hash = $table->getHash(); + foreach ($hash as $row) { + $key = $row['name'] . '|' . $row['role'] .' @ '. $row['institution']; + $data[$key] = true; + } + + // get identities form page + $searchResult = $page->findAll('xpath', "//tr[./td]"); + foreach ($searchResult as $result) { + $name = $result->find('css', 'td:nth-of-type(1)')->getText(); + $role = $result->find('css', 'td:nth-of-type(4)')->getText(); + $key = $name . '|' . $role; + + if (!array_key_exists($key, $data)) { + throw new Exception(sprintf('Unexpected user found on page: "%s"', $key)); + } + + unset($data[$key]); + } + + // check if all are found + if (!empty($data)) { + throw new Exception(sprintf('User(s) not found on page: "%s"', json_encode(array_keys($data)))); + } + } + + /** + * @Given /^The institution configuration should be:$/ + */ + public function theInstitutionConfigurationShouldBe(TableNode $table) + { + $page = $this->minkContext->getSession()->getPage(); + + $data = []; + $hash = $table->getColumnsHash(); + foreach ($hash as $row) { + $key = $row['Label'] . '|' . $row['Value']; + $data[$key] = true; + } + + // get identities form page + $searchResult = $page->findAll('xpath', "//tr"); + foreach ($searchResult as $result) { + $label = $result->find('css', 'th')->getText(); + $value = $result->find('css', 'td')->getText(); + $key = sprintf("%s|%s", $label, $value); + + if (!array_key_exists($key, $data)) { + throw new Exception(sprintf('Unexpected configuration found: "%s"', $key)); + } + unset($data[$key]); + } + + // Check if all are found + if (!empty($data)) { + throw new Exception(sprintf('Configuration options that are not found on page: "%s"', json_encode(array_keys($data)))); + } + } + /** + * @Given /^I should see the following profile:$/ + * @param TableNode $table + */ + public function iShouldSeeTheFollowingProfile(TableNode $table) + { + $page = $this->minkContext->getSession()->getPage(); + + // build hashmap to check identities + $data = []; + $hash = $table->getHash(); + foreach ($hash as $row) { + $key = sprintf("%s|%s", $row['Label'], $row['Value']); + $data[$key] = true; + } + + // Load the table data from the page + $searchResult = $page->findAll('xpath', "//tr"); + foreach ($searchResult as $result) { + $label = $result->find('css', 'th')->getText(); + $value = $result->find('css', 'td')->getText(); + $key = sprintf("%s|%s", $label, $value); + + if (!array_key_exists($key, $data)) { + throw new Exception(sprintf('Unexpected profile data found on page: "%s"', $key)); + } + + unset($data[$key]); + } + // check if all are found + if (!empty($data)) { + throw new Exception(sprintf('Missing profile data: "%s"', json_encode(array_keys($data)))); + } + } + + /** + * @When I enter the Yubikey OTP during RA vetting + */ + public function iEnterTheYubikeyOtpOnRaVetting() + { + $this->minkContext->fillField('ra_verify_yubikey_public_id_otp', 'bogus-otp-we-use-a-mock-yubikey-service'); + $form = $this->minkContext->getSession()->getPage()->find('css', 'form[name="ra_verify_yubikey_public_id"]'); + $form->submit(); + } + + /** + * @Given /^I verify the users identity by filling the last 6 characters of the document\-number$/ + */ + public function iVerifyTheUsersIdentityByFillingTheLastCharactersOfTheDocumentNumber() + { + $this->minkContext->fillField('ra_verify_identity_documentNumber', '654321'); + $this->minkContext->checkOption('ra_verify_identity_identityVerified'); + $this->minkContext->pressButton('Verify identity'); + } + + private function iSwitchLocaleTo(string $newLocale): void + { + $page = $this->minkContext->getSession()->getPage(); + $selectElement = $page->find('css', '#stepup_switch_locale_locale'); + $selectElement->selectOption($newLocale); + $form = $page->find('css', 'form[name="stepup_switch_locale"]'); + $form->submit(); + } + + private function diePrintingContent() + { + echo $this->minkContext->getSession()->getCurrentUrl(); + echo $this->minkContext->getSession()->getPage()->getContent(); + die; + } +} diff --git a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php new file mode 100644 index 0000000..0c19541 --- /dev/null +++ b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php @@ -0,0 +1,462 @@ +spTestUrl = $spTestUrl; + } + + /** + * @BeforeScenario + */ + public function gatherContexts(BeforeScenarioScope $scope) + { + $environment = $scope->getEnvironment(); + + $this->minkContext = $environment->getContext(MinkContext::class); + } + + /** + * @Given a service provider configured for second-factor-only + */ + public function configureServiceProviderForSecondFactorOnly() + { + $this->activeIdp = self::SFO_IDP; + $this->activeSp = self::SFO_SP; + $this->requiredLoa = 2; + } + + /** + * @Given a service provider configured for single-signon + */ + public function configureServiceProviderForSingleSignOn() + { + $this->activeIdp = self::SSO_IDP; + $this->activeSp = self::SSO_SP; + $this->requiredLoa = 2; + } + + /** + * @When I visit the service provider + */ + public function visitServiceProvider() + { + $this->minkContext->visit($this->spTestUrl); + + $this->minkContext->fillField('idp', $this->activeIdp); + $this->minkContext->fillField('sp', $this->activeSp); + $this->minkContext->fillField('loa', $this->requiredLoa); + + if ($this->activeIdp === self::SFO_IDP) { + $this->minkContext->fillField('subject', self::TEST_NAMEID); + } + $this->minkContext->pressButton('Login'); + } + + private function fillField($session, $field, $value) + { + $field = $this->fixStepArgument($field); + $value = $this->fixStepArgument($value); + $this->minkContext->getSession($session)->getPage()->fillField($field, $value); + } + private function fixStepArgument($argument) + { + return str_replace('\\"', '"', $argument); + } + + /** + * @Given the service provider requires no second factor + */ + public function setImplicitLoaOnServiceProvider() + { + $this->requiredLoa = 1; + } + + /** + * @When I verify the :arg1 second factor + */ + public function verifySpecifiedSecondFactor($tokenType, $smsChallenge = null) + { + switch ($tokenType){ + case "sms": + // Pass through acs + $this->minkContext->pressButton('Submit'); + $this->authenticateUserSmsInGateway($smsChallenge); + break; + case "yubikey": + $this->authenticateUserYubikeyInGateway(); + break; + case "dummy": + $this->authenticateUserInDummyGsspApplication(); + break; + default: + throw new Exception( + sprintf( + 'Second factor type of "%s" is not yet supported in the tests.', + $tokenType + ) + ); + break; + } + } + + /** + * @When I verify the Yubikey second factor + */ + public function verifyYuikeySecondFactor() + { + $this->authenticateUserYubikeyInGateway(); + } + + public function verifyGsspSecondFactor() + { + $this->authenticateUserGsspInGateway(); + } + + /** + * @When I cancel the :arg1 second factor authentication + */ + public function cancelSecondFactorAuthentication($tokenType) + { + switch ($tokenType){ + case "yubikey": + $this->cancelYubikeySsoAuthentication(); + break; + case "dummy": + $this->cancelAuthenticationInDummyGsspApplication(); + break; + default: + throw new Exception( + sprintf( + 'Second factor type of "%s" is not yet supported in the tests.', + $tokenType + ) + ); + break; + } + } + + /** + * @Then second factor authentication is not initiated + */ + public function secondFactorAuthenticationIsNotInitiated() + { + $this->passTroughGatewaySsoAssertionConsumerService(); + } + + public function selectDummySecondFactorOnTokenSelectionScreen() + { + $this->minkContext->pressButton('gateway_choose_second_factor_choose_dummy'); + } + + public function selectYubikeySecondFactorOnTokenSelectionScreen() + { + $this->minkContext->pressButton('gateway_choose_second_factor_choose_yubikey'); + } + + public function authenticateUserInDummyGsspApplication() + { + $this->minkContext->assertPageAddress('http://localhost:1234/authentication'); + + // Trigger the dummy authentication action. + $this->minkContext->pressButton('Authenticate user'); + + // Pass through the 'return to sp' redirection page. + $this->minkContext->pressButton('Submit'); + } + + public function authenticateUserYubikeyInGateway() + { + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/verify-second-factor/sso/yubikey'); + + // Give an OTP + $this->minkContext->fillField('gateway_verify_yubikey_otp_otp', 'ccccccdhgrbtucnfhrhltvfkchlnnrndcbnfnnljjdgf'); + // Simulate the enter press the yubikey otp generator + $form = $this->minkContext->getSession()->getPage()->find('css', '[id="gateway_verify_yubikey_otp_otp"]'); + if (!$form) { + throw new ElementNotFoundException('Yubikey OTP Submit form could not be found on the page'); + } + $this->minkContext->pressButton('gateway_verify_yubikey_otp_submit'); + // Pass through the 'return to sp' redirection page. + $this->minkContext->pressButton('Submit'); + } + + public function authenticateUserGsspInGateway() + { + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/authentication'); + $this->minkContext->pressButton('button_authenticate'); + // Pass through the 'return to sp' redirection page. + $this->minkContext->pressButton('Submit'); + $this->minkContext->pressButton('Submit'); + } + + public function authenticateUserSmsInGateway(string $challenge) + { + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/verify-second-factor/sms/verify-challenge'); + // Fill the challenge + $this->minkContext->fillField('gateway_verify_sms_challenge_challenge', $challenge); + // Submit the form + $this->minkContext->pressButton('Verify code'); + $this->minkContext->assertResponseNotContains('stepup.verify_possession_of_phone_command.challenge.may_not_be_empty'); + } + + public function cancelAuthenticationInDummyGsspApplication() + { + $this->minkContext->assertPageAddress('http://localhost:1234/authentication'); + + // Cancel the dummy authentication action. + $this->minkContext->pressButton('Return authentication failed'); + + // Pass through the 'return to sp' redirection page. + $this->minkContext->pressButton('Submit'); + } + + public function cancelYubikeySsoAuthentication() + { + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/verify-second-factor/sso/yubikey'); + // Cancel the yubikey authentication action. + $this->minkContext->pressButton('Cancel'); + // Pass through the 'return to sp' redirection page. + $this->minkContext->pressButton('Submit'); + } + + public function passTroughGatewaySsoAssertionConsumerService() + { + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/authentication/consume-assertion'); + + $this->minkContext->pressButton('Submit'); + } + + public function passTroughGatewayProxyAssertionConsumerService() + { + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/gssp/dummy/consume-assertion'); + + $this->minkContext->pressButton('Submit'); + } + + /** + * @When I authenticate with the identity provider + */ + public function authenticateWithIdentityProvider() + { + $this->minkContext->assertPageAddress('https://ssp.dev.openconext.local/simplesaml/module.php/core/loginuserpass'); + + $this->minkContext->fillField('username', 'user-a1'); + $this->minkContext->fillField('password', 'user-a1'); + + $this->minkContext->pressButton('Login'); + $this->minkContext->pressButton('Yes, continue'); + + } + + /** + * @When I authenticate as :arg1 with the identity provider + */ + public function authenticateWithIdentityProviderFor($userName) + { + $this->minkContext->assertPageAddress('https://ssp.dev.openconext.local/simplesaml/module.php/core/loginuserpass'); + + $this->minkContext->fillField('username', $userName); + $this->minkContext->fillField('password', $userName); + + $this->minkContext->pressButton('Login'); + $this->minkContext->pressButton('Yes, continue'); + + } + + public function authenticateWithIdentityProviderForWithStepup($userName) + { + $this->minkContext->assertPageAddress('https://ssp.dev.openconext.local/simplesaml/module.php/core/loginuserpass'); + + $this->minkContext->fillField('username', $userName); + $this->minkContext->fillField('password', $userName); + + $this->minkContext->pressButton('Login'); + $this->minkContext->pressButton('Yes, continue'); + } + + private function passTroughIdentityProviderAssertionConsumerService() + { + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/authentication/consume-assertion'); + + $this->minkContext->assertPageNotContainsText('Incorrect username or password'); + $this->minkContext->pressButton('Submit'); + } + + /** + * @Then I am logged on the service provider + */ + public function assertLoggedInOnServiceProvider() + { + $this->minkContext->assertPageAddress('https://ssp.dev.openconext.local/simplesaml/sp.php'); + + $this->minkContext->assertPageContainsText('You are logged in to SP'); + } + + /** + * @Then I see an error at the service provider + */ + public function assertErrorAtServiceProvider() + { + $this->minkContext->assertPageAddress('https://ssp.dev.openconext.local/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp'); + + $this->minkContext->assertPageContainsText( + sprintf('Unhandled exception') + ); + + $this->minkContext->assertPageNotContainsText( + sprintf('You are logged in to SP') + ); + } + + /** + * @When I prepare an SFO authentication as :arg1 + */ + public function prepareSfoAuthentication($nameId) + { + $this->minkContext->getSession('second')->visit($this->spTestUrl); + + $this->fillField('second', 'idp', $this->activeIdp); + $this->fillField('second','sp', $this->activeSp); + $this->fillField('second','loa', $this->requiredLoa); + $this->fillField('second', 'subject', $nameId); + } + + /** + * @Given I start and intercept the SFO authentication + */ + public function iStartASMSSFOAuthentication() + { + // To intercept the AuthNRequest, instruct the 'browser' not to auto-follow redirects + $client = $this->minkContext->getSession('second')->getDriver()->getClient(); + $client->followRedirects(false); + $this->minkContext->getSession('second')->getPage()->pressButton('Login'); + // Jump from SSP SP to Gateway (we are interested in that AR) + $client->followRedirect(); + // Catch the Url containing he AuthNRequest, removing the trailing slash + $this->storedAuthnRequest = $this->minkContext->getSession('second')->getCurrentUrl(); + // And back to normal + $client->followRedirects(true); + } + + /** + * @Given I start the stored SFO session in the victims session remembering the challenge for :arg1 + */ + public function victimizeTheStoredSFORequest($phoneNumber) + { + if ($this->storedAuthnRequest === null) { + throw new RuntimeException('There is no stored authentication request. First run step definition: "I start and intercept a SMS SFO authentication"'); + } + $this->minkContext->visit($this->storedAuthnRequest); + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/verify-second-factor/sms/verify-challenge?authenticationMode=sfo'); + $this->storedChallengeCode[$phoneNumber] = $this->fetchSmsChallengeFromCookie($phoneNumber); + } + + /** + * @Given I use the stored SMS verification code for :arg1 + */ + public function iUseTheStoredVerificationCode($phoneNumber) + { + if (!isset($this->storedChallengeCode[$phoneNumber])) { + throw new RuntimeException('There is no stored SMS challenge available for this phone number.'); + } + $this->authenticateUserSmsInGateway($this->storedChallengeCode[$phoneNumber]); + } + + private function fetchSmsChallengeFromCookie($phoneNumber): string + { + $cookies = $this->minkContext + ->getSession() + ->getDriver() + ->getClient() + ->getCookieJar() + ->all(); + $expectedCookieName = sprintf("%s%s", 'smoketest-sms-service-', $phoneNumber); + $bodyPattern = '/^Your.SMS.code:.([A-Z-0-9]+)$/'; + foreach ($cookies as $cookie) { + if ($cookie->getName() === $expectedCookieName) { + $bodyMatches = []; + preg_match($bodyPattern, $cookie->getValue(), $bodyMatches); + return array_pop($bodyMatches); + } + } + throw new RuntimeException('SMS verification code was not found in smoketest cookie'); + } + + /** + * @Then I start an SMS SSO session for :arg1 with verification code for :arg2 + */ + public function iStartAnSmsSSOSessionFor($userName, $phoneNumber) + { + $this->configureServiceProviderForSingleSignOn(); + $this->visitServiceProvider(); + // Pass through Gateway (already authenticated) + $this->minkContext->pressButton('Submit'); + // Choose SMS token on WAYG + $this->minkContext->pressButton('gateway_choose_second_factor[choose_sms]'); + } + + /** + * @Then /^The verification code is invalid$/ + */ + public function theVerificationCodeIsInvalid() + { + $this->minkContext->assertResponseContains('This code is not correct. Please try again or request a new code.'); + } + + private function diePrintingContent() + { + echo $this->minkContext->getSession()->getCurrentUrl(); + echo $this->minkContext->getSession()->getPage()->getContent(); + die; + } +} diff --git a/stepup/tests/behat/features/bootstrap/SelfServiceContext.php b/stepup/tests/behat/features/bootstrap/SelfServiceContext.php new file mode 100644 index 0000000..e591679 --- /dev/null +++ b/stepup/tests/behat/features/bootstrap/SelfServiceContext.php @@ -0,0 +1,572 @@ +selfServiceUrl = $selfServiceUrl; + $this->mailCatcherUrl = $mailCatcherUrl; + } + + /** + * @BeforeScenario + */ + public function gatherContexts(BeforeScenarioScope $scope) + { + $environment = $scope->getEnvironment(); + + $this->minkContext = $environment->getContext(MinkContext::class); + $this->authContext = $environment->getContext(SecondFactorAuthContext::class); + } + + /** + * @Given I am logged in into the selfservice portal + */ + public function loginIntoSelfService() + { + $this->minkContext->visit($this->selfServiceUrl); + + $this->authContext->authenticateWithIdentityProvider(); + $this->authContext->passTroughGatewaySsoAssertionConsumerService(); + + $this->minkContext->assertPageContainsText('Registration Portal'); + } + + /** + * @Given /^I am logged in into the selfservice portal as "([^"]*)"$/ + */ + public function iAmLoggedInIntoTheSelfServicePortalAs($userName) + { + // We visit the Self Service location url + $this->minkContext->visit($this->selfServiceUrl); + $this->authContext->authenticateWithIdentityProviderFor($userName); + $this->authContext->passTroughGatewaySsoAssertionConsumerService(); + $this->iSwitchLocaleTo('English'); + $this->minkContext->assertPageContainsText('Registration Portal'); + } + + /** + * @When I register a new :tokenType token + */ + public function registerNewToken(string $tokenType) + { + $this->minkContext->assertPageAddress('/registration/select-token'); + + switch ($tokenType) { + case 'Yubikey': + $this->minkContext->getSession() + ->getPage() + ->find('css', '[href="/registration/yubikey/prove-possession"]')->click(); + $this->minkContext->assertPageAddress('/registration/yubikey/prove-possession'); + $this->minkContext->assertPageContainsText('Link your YubiKey'); + $this->minkContext->fillField('ss_prove_yubikey_possession_otp', 'ccccccdhgrbtfddefpkffhkkukbgfcdilhiltrrncmig'); + $page = $this->minkContext->getSession()->getPage(); + $form = $page->find('css', 'form[name="ss_prove_yubikey_possession"]'); + $form->submit(); + break; + case 'SMS': + // Select the SMS second factor type + $this->minkContext->getSession() + ->getPage() + ->find('css', '[href="/registration/sms/send-challenge"]')->click(); + $this->minkContext->assertPageAddress('/registration/sms/send-challenge'); + // Start registration + $this->minkContext->assertPageContainsText('Send code'); + $this->minkContext->fillField('ss_send_sms_challenge_subscriber', '612345678'); + $this->minkContext->pressButton('Send code'); + + // Now we should be on the prove possession page where we enter our OTP + $this->minkContext->assertPageAddress('/registration/sms/prove-possession'); + $this->minkContext->assertPageContainsText('Enter SMS code'); + $this->minkContext->fillField('ss_verify_sms_challenge_challenge', '999'); + + $this->minkContext->pressButton('Verify'); + break; + case 'Demo GSSP': + // Select the SMS second factor type + $page = $this->minkContext->getSession()->getPage(); + $form = $page->find('css', 'form[action="/registration/gssf/demo_gssp/authenticate"]'); + $form->submit(); + + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/registration'); + // Start registration + $this->minkContext->assertPageContainsText('Register user'); + $this->minkContext->pressButton('Register user'); + + // Pass through the demogssp + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/saml/sso_return'); + $this->minkContext->pressButton('Submit'); + // Pass through the gateway + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/gssp/demo_gssp/consume-assertion'); + $this->minkContext->pressButton('Submit'); + // Now we should be back at the SelfService + break; + default: + throw new Exception(sprintf('The %s token type is not supported', $tokenType)); + } + } + + /** + * @When I self-vet a new SMS token with my Yubikey token + + */ + public function selfVetNewSmsToken() + { + $this->minkContext->visit($this->selfServiceUrl); + $this->minkContext->assertPageAddress('/overview'); + + $this->minkContext->assertPageContainsText('The following tokens are registered for your account'); + $this->minkContext->assertPageContainsText('Yubikey'); + + $this->minkContext->visit('/registration/select-token'); + + // Select the sms second factor type + $this->minkContext->getSession() + ->getPage() + ->find('css', '[href="/registration/sms/send-challenge"]')->click(); + $this->minkContext->assertPageAddress('/registration/sms/send-challenge'); + + // Start registration + $this->minkContext->assertPageContainsText('Send SMS code'); + $this->minkContext->fillField('ss_send_sms_challenge_subscriber', '612345678'); + $this->minkContext->pressButton('Send code'); + + $this->minkContext->assertPageContainsText('Enter the code that was sent to your phone'); + $this->minkContext->fillField('ss_verify_sms_challenge_challenge', '999'); + $this->minkContext->pressButton('Verify'); + + $this->minkContext->visit( + $this->getEmailVerificationUrl() + ); + // Now we should be on the choose vetting page + $this->minkContext->assertPageContainsText('Use your existing token'); + $page = $this->minkContext->getSession()->getPage(); + $form = $page->find('css', 'form[action$="self-vet"]'); + $form->submit(); + $this->minkContext->pressButton('Yes, continue'); + $this->minkContext->pressButton('Submit'); + $this->authContext->authenticateUserYubikeyInGateway(); + } + + /** + * @Given /^I try to self\-vet a new Yubikey token with my SMS token$/ + */ + public function iTryToSelfVetANewYubikeyTokenWithMySMSToken() + { + $this->minkContext->visit($this->selfServiceUrl); + $this->minkContext->assertPageAddress('/overview'); + + $this->minkContext->assertPageContainsText('The following tokens are registered for your account'); + $this->minkContext->assertPageContainsText('SMS'); + $this->minkContext->assertPageContainsText('+31 (0) 612345678'); + + $this->minkContext->visit('/registration/select-token'); + + // Select the sms second factor type + $this->minkContext->getSession() + ->getPage() + ->find('css', '[href="/registration/yubikey/prove-possession"]')->click(); + $this->minkContext->assertPageAddress('/registration/yubikey/prove-possession'); + + // Start registration + $this->minkContext->assertPageContainsText('Link your YubiKey'); + $this->minkContext->fillField('ss_prove_yubikey_possession_otp', 'ccccccdhgrbtfddefpkffhkkukbgfcdilhiltrrncmig'); + $page = $this->minkContext->getSession()->getPage(); + $form = $page->find('css', 'form[name="ss_prove_yubikey_possession"]'); + $form->submit(); + + } + + /** + * @When I verify my e-mail address and choose the :vettingType vetting type + */ + public function verifyEmailAddress(string $vettingType) + { + ## And we should now be on the mail verification page + $this->minkContext->assertPageContainsText('Verify your e-mail'); + $this->minkContext->assertPageContainsText('Check your inbox'); + + $this->minkContext->visit( + $this->getEmailVerificationUrl() + ); + switch ($vettingType) { + case "RA vetting": + $url = $this->minkContext->getSession()->getCurrentUrl(); + if (strpos($url, '/registration-email-sent') === false) { + $this->iChooseToActivateMyTokenUsingServiceDeskVetting(); + } + $this->minkContext->assertPageContainsText('Thank you for registering your token.'); + + $page = $this->minkContext->getSession()->getPage(); + $activationCodeCell = $page->find('xpath', '//th[text()="Activation code"]/../td'); + if (!$activationCodeCell) { + throw new Exception('Could not find a activation code table on the page'); + } + + $url = $this->minkContext->getSession()->getCurrentUrl(); + $matches = []; + preg_match('#[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}#', $url, $matches); + if (empty($matches)) { + throw new Exception('Could not find a valid second factor verification id in the url'); + } + $this->activationCode = $activationCodeCell->getText(); + $this->verifiedSecondFactorId = reset($matches); + + if (!preg_match('#[A-Z0-9]{8}#', $this->activationCode)) { + throw new Exception('Could not find a valid activation code'); + } + break; + case "Self Asserted Token registration": + $this->iChooseToActivateMyTokenUsingSat(); + break; + case "Self vetting": + // Select the sms second factor type + $this->minkContext->getSession() + ->getPage() + ->find('css', '[href="/registration/sms/send-challenge"]')->click(); + $this->minkContext->assertPageAddress('/registration/sms/send-challenge'); + + // Start registration + $this->minkContext->assertPageContainsText('Send SMS code'); + $this->minkContext->fillField('ss_send_sms_challenge_subscriber', '612345678'); + $this->minkContext->pressButton('Send code'); + + $this->minkContext->assertPageContainsText('Enter the code that was sent to your phone'); + $this->minkContext->fillField('ss_verify_sms_challenge_challenge', '999'); + $this->minkContext->pressButton('Verify'); + + + $this->iChooseToActivateMyTokenUsingSat(); + break; + default: + throw new Exception(sprintf('Vetting type "%s" is not supported', $vettingType)); + } + } + + /** + * @When I choose the :vettingType vetting type + */ + public function chooseVettingType(string $vettingType) + { + switch ($vettingType) { + case "RA vetting": + $this->iChooseToActivateMyTokenUsingServiceDeskVetting(); + $this->minkContext->assertPageContainsText('Thank you for registering your token.'); + + $page = $this->minkContext->getSession()->getPage(); + $activationCodeCell = $page->find('xpath', '//th[text()="Activation code"]/../td'); + if (!$activationCodeCell) { + throw new Exception('Could not find a activation code table on the page'); + } + + $url = $this->minkContext->getSession()->getCurrentUrl(); + $matches = []; + preg_match('#[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}#', $url, $matches); + if (empty($matches)) { + throw new Exception('Could not find a valid second factor verification id in the url'); + } + $this->activationCode = $activationCodeCell->getText(); + $this->verifiedSecondFactorId = reset($matches); + + if (!preg_match('#[A-Z0-9]{8}#', $this->activationCode)) { + throw new Exception('Could not find a valid activation code'); + } + break; + case "Self Asserted Token registration": + $this->iChooseToActivateMyTokenUsingSat(); + break; + default: + throw new Exception(sprintf('Vetting type "%s" is not supported', $vettingType)); + } + } + + /** + * @Given I vet my :tokenType second factor in selfservice + */ + public function iVetMySecondFactorInSelfService(string $tokenType) + { + $url = $this->minkContext->getSession()->getCurrentUrl(); + $matches = []; + preg_match('#[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}#', $url, $matches); + if (empty($matches)) { + throw new Exception('Could not find a valid second factor verification id in the url'); + } + $secondFactorId = reset($matches); + $this->minkContext->assertPageAddress(sprintf('/second-factor/%s/new-recovery-token', $secondFactorId)); + + $page = $this->minkContext->getSession()->getPage(); + $form = $page->find('css', 'form[name="safe-store"]'); + $form->submit(); + // Todo store the safe store password for later use? + + $this->minkContext->assertPageAddress(sprintf('/second-factor/%s/safe-store', $secondFactorId)); + // Promise we safely stored the secret + $this->minkContext->checkOption('ss_promise_recovery_token_possession_promisedPossession'); + preg_match('#[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}#', $url, $matches); + if (empty($matches)) { + throw new Exception('Could not find a valid second factor verification id in the url'); + } + // Update the SF id, it now refers to the vetted second factor id + $secondFactorId = reset($matches); + $this->minkContext->pressButton('Continue'); + // We are back on the overview page. The revoke button is on the page (indicating the token was vetted) + $this->minkContext->assertPageAddress('/overview'); + echo $secondFactorId; + $this->minkContext->assertResponseContains(sprintf('/second-factor/vetted/%s/revoke', $secondFactorId)); + // The recovery token should also be present + $this->minkContext->assertPageContainsText('Recovery methods'); + $this->minkContext->assertPageContainsText('Recovery code'); + $this->minkContext->assertResponseContains('/recovery-token/delete/'); + } + + public function iChooseToActivateMyTokenUsingServiceDeskVetting() + { + $this->minkContext->assertPageContainsText('Activate your token'); + $this->minkContext->pressButton('ra-vetting-button'); + } + public function iChooseToActivateMyTokenUsingSAT() + { + $this->minkContext->assertPageContainsText('Activate your token yourself'); + $this->minkContext->pressButton('sat-button'); + } + + /** + * @Then I can add an :recoveryTokenType recovery token using :tokenType + */ + public function iCanAddAnRecoveryToken(string $recoveryTokenType, string $tokenType) + { + $this->minkContext->assertPageContainsText('Add recovery method'); + $this->minkContext->clickLink('Add recovery method'); + switch ($recoveryTokenType){ + case 'SMS': + $this->minkContext->assertPageContainsText('You\'ll receive a text message with a verification code'); + $page = $this->minkContext->getSession()->getPage(); + $form = $page->find('css', 'form[name="sms"]'); + $form->submit(); + $this->provePossession($tokenType); + // Now you need to register your SMS recovery token + + $this->minkContext->assertPageContainsText('Register an SMS recovery token'); + $this->minkContext->fillField('ss_send_sms_challenge_subscriber', '615056898'); + $this->minkContext->pressButton('Send code'); + + $this->minkContext->assertPageAddress('/recovery-token/prove-sms-possession'); + $this->minkContext->assertPageContainsText('Enter the code that was sent to your phone'); + $this->minkContext->fillField('ss_verify_sms_challenge_challenge', '123456'); + $this->minkContext->pressButton('Verify'); + break; + default: + throw new Exception(sprintf('Recovery token type %s is not supported', $recoveryTokenType)); + } + } + + private function provePossession(string $tokenType) + { + switch ($tokenType) { + case "Demo GSSP": + // We first need to prove we are in possession of our 2nd factor + $this->minkContext->pressButton('Yes, continue'); + // Press the Authenticate button on the Demo authentication page + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/authentication'); + $this->minkContext->pressButton('button_authenticate'); + // Pass through the Demo Gssp redirection page. + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/saml/sso_return'); + $this->minkContext->pressButton('Submit'); + // Pass through the 'return to sp' redirection page. + $this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/gssp/demo_gssp/consume-assertion'); + $this->minkContext->pressButton('Submit'); + + break; + case "Yubikey": + $this->minkContext->pressButton('Yes, continue'); + $this->performYubikeyAuthentication(); + break; + + default: + throw new Exception(sprintf('Prove possession is not yet supported for token type %s', $tokenType)); + } + } + + /** + * @Then I can not add a :recoveryTokenType recovery token + */ + public function iCanNotAddARecoveryToken($recoveryTokenType) + { + $this->minkContext->visit('/overview'); + $this->minkContext->assertPageNotContainsText('Add recovery method'); + } + + /** + * @Then :nofRecoveryTokens recovery tokens are activated + */ + public function numberOfTokensRegistered(int $nofRecoveryTokens) + { + $page = $this->minkContext->getMink()->getSession()->getPage(); + $tokenTypes = $page->findAll('css', 'tr[data-test_tokentype]'); + + if (empty($tokenTypes)) { + throw new Exception('No active recovery tokens found on the page'); + } + + if (count($tokenTypes) != $nofRecoveryTokens) { + throw new Exception( + sprintf( + 'Excpected to find %d recovery tokens on the page, actually found %d tokens', + $nofRecoveryTokens, + count($tokenTypes) + ) + ); + } + } + + private function performYubikeyAuthentication() + { + $this->minkContext->fillField('gateway_verify_yubikey_otp_otp', 'ccccccdhgrbtfddefpkffhkkukbgfcdilhiltrrncmig'); + $page = $this->minkContext->getSession()->getPage(); + $form = $page->find('css', 'form[name="gateway_verify_yubikey_otp"]'); + $form->submit(); + $this->minkContext->pressButton('Submit'); + } + + /** + * @Given /^I visit the "([^"]*)" page in the selfservice portal$/ + */ + public function iVisitAPageInTheSelfServiceEnvironment($uri) + { + // We visit the SS location url + $this->minkContext->visit($this->selfServiceUrl.'/'.$uri); + } + + private function iSwitchLocaleTo(string $newLocale): void + { + $page = $this->minkContext->getSession()->getPage(); + $selectElement = $page->find('css', '#stepup_switch_locale_locale'); + $selectElement->selectOption($newLocale); + $form = $page->find('css', 'form[name="stepup_switch_locale"]'); + $form->submit(); + } + + private function getEmailVerificationUrl() + { + $body = $this->getLastSentEmail(); + $body = str_replace("\r", '', $body); + $body = str_replace("=\n", '', $body); + $body = str_replace("=3D", '=', $body); + + if (!preg_match('#(https://selfservice.dev.openconext.local/verify-email\?n=[a-f0-9]+)#', $body, $matches)) { + throw new Exception('Unable to find email verification link in message'); + } + + return $matches[1]; + } + + private function getLastSentEmail() + { + $response = file_get_contents($this->mailCatcherUrl); + + if (!$response) { + throw new Exception( + 'Unable to read e-mail - is mailcatcher active?' + ); + } + + $messages = json_decode($response); + if (!$messages) { + throw new Exception( + 'Unable to parse mailcatcher response' + ); + } + + if (empty($messages)) { + throw new Exception( + 'No mail received by mailcatcher!' + ); + } + + $firstMessage = array_pop($messages); + + return $this->getEmailById( + $firstMessage->id + ); + } + + + private function getEmailById($id) + { + $response = file_get_contents( + sprintf( + '%s/%d.html', + rtrim($this->mailCatcherUrl, '/'), + $id + ) + ); + + if (!$response) { + throw new Exception( + 'Unable to read e-mail message - is mailcatcher active?' + ); + } + + return $response; + } + + /** + * @return string + */ + public function getActivationCode() + { + return $this->activationCode; + } + + /** + * @return string + */ + public function getVerifiedSecondFactorId() + { + return $this->verifiedSecondFactorId; + } + + private function diePrintingContent() + { + echo $this->minkContext->getSession()->getCurrentUrl(); + echo $this->minkContext->getSession()->getPage()->getContent(); + die; + } +} diff --git a/stepup/tests/behat/features/fga-use-case-a.feature b/stepup/tests/behat/features/fga-use-case-a.feature new file mode 100644 index 0000000..feeac55 --- /dev/null +++ b/stepup/tests/behat/features/fga-use-case-a.feature @@ -0,0 +1,70 @@ +Feature: Use case A: Institutions with few (10-20) users using a third party vetting service + For institutions with few users that are using Stepup, for which setting up and maintaining a local vetting + structure is relatively expensive, we want to allow a third party to do the vetting whereby the RA's + are associated with this third party. + + Scenario: Setup of the institution configuration and test users + Given I have the payload + """ + { + "dev.openconext.local": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2 + }, + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": [ + "dev.openconext.local" + ] + }, + "institution-d.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": [ + "dev.openconext.local" + ] + }, + "institution-e.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": [ + "dev.openconext.local" + ] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + And a user "Usain Sergei" identified by "urn:collab:person:dev.openconext.local:joe--ra" from institution "dev.openconext.local" + And the user "urn:collab:person:dev.openconext.local:joe--ra" has a vetted "yubikey" with identifier "00000004" + And the user "urn:collab:person:dev.openconext.local:joe--ra" has the role "ra" for institution "dev.openconext.local" + And a user "Jane Jackson" identified by "urn:collab:person:institution-a.example.com:jane-a1" from institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a1" has a vetted "yubikey" with identifier "00000005" + And a user "Joe Satriani" identified by "urn:collab:person:institution-d.example.com:joe-d1" from institution "institution-d.example.com" + And the user "urn:collab:person:institution-d.example.com:joe-d1" has a verified "yubikey" with registration code "1234ABCD" + And a user "Joe Perry" identified by "urn:collab:person:institution-e.example.com:joe-e1" from institution "institution-e.example.com" + And the user "urn:collab:person:institution-e.example.com:joe-e1" has a verified "yubikey" with registration code "9876WXYZ" + + Scenario: The third party RA user can vet tokens from other institutions + Given I am logged in into the ra portal as "joe--ra" with a "yubikey" token + When I search for "1234ABCD" on the token activation page + Then I should see "Please connect the user's personal Yubikey with your computer" + When I search for "9876WXYZ" on the token activation page + Then I should see "Please connect the user's personal Yubikey with your computer" diff --git a/stepup/tests/behat/features/fga-use-case-b.feature b/stepup/tests/behat/features/fga-use-case-b.feature new file mode 100644 index 0000000..d79f447 --- /dev/null +++ b/stepup/tests/behat/features/fga-use-case-b.feature @@ -0,0 +1,61 @@ +Feature: Use case B: Institutions sharing vetting locations + Allow users from institutions that cannot easily visit the vetting location at the institution, e.g. because + they work remotely or abroad, to use the RA services of another institution that is closer to their location. By + pooling the RAs from different geographical locations, the access for users to RA services is improved. + + Scenario: Scenario: Setup of the institution configuration and test users + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": [ + "institution-a.example.com", + "institution-d.example.com" + ] + }, + "institution-d.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": [ + "institution-a.example.com", + "institution-d.example.com" + ] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + And a user "RA institution A" identified by "urn:collab:person:institution-a.example.com:joe-a-ra" from institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:joe-a-ra" has a vetted "yubikey" with identifier "00000004" + And the user "urn:collab:person:institution-a.example.com:joe-a-ra" has the role "ra" for institution "institution-a.example.com" + And a user "RA institution D" identified by "urn:collab:person:institution-d.example.com:joe-d-ra" from institution "institution-d.example.com" + And the user "urn:collab:person:institution-d.example.com:joe-d-ra" has a vetted "yubikey" with identifier "00000005" + And the user "urn:collab:person:institution-d.example.com:joe-d-ra" has the role "ra" for institution "institution-d.example.com" + And a user "Jane Jackson" identified by "urn:collab:person:institution-a.example.com:jane-a1" from institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a1" has a verified "yubikey" with registration code "1234ABCD" + And a user "Joe Satriani" identified by "urn:collab:person:institution-d.example.com:joe-d1" from institution "institution-d.example.com" + And the user "urn:collab:person:institution-d.example.com:joe-d1" has a verified "yubikey" with registration code "ABCD1234" + + Scenario: The institution A RA can vet identities from institution A and D + And I am logged in into the ra portal as "joe-a-ra" with a "yubikey" token + When I search for "ABCD1234" on the token activation page + Then I should see "Please connect the user's personal Yubikey with your computer" + When I search for "1234ABCD" on the token activation page + Then I should see "Please connect the user's personal Yubikey with your computer" + + Scenario: The institution D RA can vet identities from institution A and D + Given I am logged in into the ra portal as "joe-d-ra" with a "yubikey" token + When I search for "ABCD1234" on the token activation page + Then I should see "Please connect the user's personal Yubikey with your computer" + When I search for "1234ABCD" on the token activation page + Then I should see "Please connect the user's personal Yubikey with your computer" diff --git a/stepup/tests/behat/features/fga-use-case-c.feature b/stepup/tests/behat/features/fga-use-case-c.feature new file mode 100644 index 0000000..c175c1f --- /dev/null +++ b/stepup/tests/behat/features/fga-use-case-c.feature @@ -0,0 +1,78 @@ +Feature: Use case C: Closely cooperating institutions + Two or more Institutions that are working (closely) together and that want to share their vetting + infrastructure Note: the difference with the previous use case (B) is that in this use-case (C) each institution has + fine grained control over who of the other institution may work for them. + + Note that in use-case B an institution allows all RAs from the other institution(s), it has no control over who these + are, this is decided by the RAAs for the other institution. In this usecase each institution manages all its RAA(s), + i.e. it chooses which persons from the other institution are RA. These users are not required to be an RA at the other + institution. + + Scenario: Setup of the institution configuration and test users + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "select_raa": [ + "institution-a.example.com", + "institution-d.example.com" + ], + "use_raa": [ + "institution-a.example.com", + "institution-d.example.com" + ], + "use_ra": [ + "institution-a.example.com", + "institution-d.example.com" + ] + }, + "institution-d.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "select_raa": [ + "institution-a.example.com", + "institution-d.example.com" + ], + "use_raa": [ + "institution-d.example.com", + "institution-a.example.com" + ], + "use_ra": [ + "institution-d.example.com", + "institution-a.example.com" + ] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + And a user "joe-a-raa" identified by "urn:collab:person:institution-a.example.com:joe-a-raa" from institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has a vetted "yubikey" with identifier "00000004" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has the role "raa" for institution "institution-a.example.com" + And a user "joe-d-raa" identified by "urn:collab:person:institution-d.example.com:joe-d-raa" from institution "institution-d.example.com" + And the user "urn:collab:person:institution-d.example.com:joe-d-raa" has a vetted "yubikey" with identifier "00000005" + And the user "urn:collab:person:institution-d.example.com:joe-d-raa" has the role "raa" for institution "institution-d.example.com" + And a user "jane-a1" identified by "urn:collab:person:institution-a.example.com:jane-a1" from institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a1" has a vetted "yubikey" with identifier "00000006" + And a user "joe-d1" identified by "urn:collab:person:institution-d.example.com:joe-d1" from institution "institution-d.example.com" + And the user "urn:collab:person:institution-d.example.com:joe-d1" has a vetted "yubikey" with identifier "00000007" + + Scenario: The institution A RAA can promote identities from institution D + Given I am logged in into the ra portal as "joe-a-raa" with a "yubikey" token + When I visit the RA promotion page + Then I change the role of "joe-d1" to become "RA" for institution "institution-d.example.com" + + Scenario: The institution D RAA can promote identities from institution A + Given I am logged in into the ra portal as "joe-d-raa" with a "yubikey" token + When I visit the RA promotion page + Then I change the role of "jane-a1" to become "RA" for institution "institution-a.example.com" diff --git a/stepup/tests/behat/features/fga-use-case-d.feature b/stepup/tests/behat/features/fga-use-case-d.feature new file mode 100644 index 0000000..06b85cc --- /dev/null +++ b/stepup/tests/behat/features/fga-use-case-d.feature @@ -0,0 +1,68 @@ +Feature: Use case D: Vetting users from a guest IdP + Allow users from a "guest" IdP (i.e. an IdP for users that do not have a relation with an institution that + warrants adding them to the institutional IdP) to be vetted. The RA(A)s to do this will necessarily need to belong to + another institution. The guest IdP itself does not have RAs or RAAs. + In this context: + - institution-a.example.com = Guest IdP + - institution-b.example.com = Vetting service + - institution-d.example.com = Vetting service + + Scenario: Setup of the institution configuration and test users - option 1 + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": [ + "institution-b.example.com" + ], + "use_raa": [ + "institution-b.example.com" + ], + "select_raa": [] + }, + "institution-b.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1 + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + + Scenario: Setup of the institution configuration and test users - option 2 + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "select_raa": [ + "institution-b.example.com" + ] + }, + "institution-b.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1 + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" diff --git a/stepup/tests/behat/features/fga-use-case-e.feature b/stepup/tests/behat/features/fga-use-case-e.feature new file mode 100644 index 0000000..eac0893 --- /dev/null +++ b/stepup/tests/behat/features/fga-use-case-e.feature @@ -0,0 +1,53 @@ +Feature: Use case E: Institution that uses multiple SHOs + An institution IdP that issues multiple SHOs, e.g. institution.org and some-related-part.institution.org. From the + perspective of Stepup these would currently become multiple institutions, which each have and manage their own RA(A)s. + This is not what the institution would want, they would want their multiple SHOs to appear as one institution + management wise. + + - institution-a.example.com= An institution with SHO additional SHOs + - institution-b.example.com = One of the additional SHOs + - institution-c.example.com = One of the additional SHOs + + Scenario: Setup of the institution configuration and test users + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2, + "select_raa": [ + "institution-a.example.com", + "institution-b.example.com", + "institution-d.example.com" + ] + }, + "institution-b.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": ["institution-a.example.com"], + "use_raa": ["institution-a.example.com"], + "select_raa": [] + }, + "institution-d.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": ["institution-a.example.com"], + "use_raa": ["institution-a.example.com"], + "select_raa": [] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" diff --git a/stepup/tests/behat/features/fga-use-case-f.feature b/stepup/tests/behat/features/fga-use-case-f.feature new file mode 100644 index 0000000..8f573db --- /dev/null +++ b/stepup/tests/behat/features/fga-use-case-f.feature @@ -0,0 +1,52 @@ +Feature: Use case F: An institution that manages Stepup for a (a group of) sister and/or daughter institutions + An institution that manages Stepup for (a group of) sister and/or daughter institutions. This use-case appears similar + to use-case A. The differences lie in the relation that the managing institution has with the institutions being + manged. RA(s)s could come from the sister/daughter institutions, but would be managed from the main institution. + + - institution-a.example.com = The parent institution + - institution-b.example.com = Sister/daughter institution + - institution-c.example.com = Sister/daughter institution + + Scenario: Setup of the institution configuration and test users + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2, + "select_raa": [ + "institution-a.example.com", + "institution-b.example.com", + "institution-d.example.com" + ] + }, + "institution-b.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": ["institution-a.example.com"], + "use_raa": ["institution-a.example.com"], + "select_raa": [] + }, + "institution-d.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 1, + "use_ra": ["institution-a.example.com"], + "use_raa": ["institution-a.example.com"], + "select_raa": [] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" diff --git a/stepup/tests/behat/features/identity.feature b/stepup/tests/behat/features/identity.feature new file mode 100644 index 0000000..5f52180 --- /dev/null +++ b/stepup/tests/behat/features/identity.feature @@ -0,0 +1,31 @@ +Feature: A (S)RA(A) user reads identities of StepUp users in the middleware API + In order to list identities + As a (S)RA(A) user + I must be able to read from the middleware API + + Scenario: A (S)RA(A) user reads identities without additional authorization context + Given I authenticate with user "ra" and password "secret" + When I request "GET /identity?institution=institution-a.example.com" + Then the api response status code should be 200 + And the "items" property should contain 3 items + + Scenario: A (S)RA(A) user reads identities of a non existent institution + Given I authenticate with user "ra" and password "secret" + When I request "GET /identity?institution=institution-x.example.com" + Then the api response status code should be 200 + And the "items" property should be an empty array + + Scenario: The admin SRAA user reads identities with authorization context + Given I authenticate with user "ra" and password "secret" + When I request "GET /identity?institution=institution-a.example.com&actorId=e9ab38c3-84a8-47e6-b371-4da5c303669a&actorInstitution=dev.openconext.local" + Then the api response status code should be 200 + And the "items" property should contain 3 items + + Scenario: The admin SRAA user reads identities with authorization context of a non existent institution + Given I authenticate with user "ra" and password "secret" + When I request "GET /identity?institution=institution-x.example.com&actorId=e9ab38c3-84a8-47e6-b371-4da5c303669a&actorInstitution=dev.openconext.local" + Then the api response status code should be 200 + And the "items" property should be an empty array + + Scenario: A very long NameID is not permitted and should not exceed the pre-configured database length for the field + Given a user "Longjohn" identified by "urn:collab:person:institution-a.example.com:thisuserhasawaytolongusernamethatexceeds255characters_thisuserhasawaytolongusernamethatexceeds255charactersthisuserhasawaytolongusernamethatexceeds255characters_thisuserhasawaytolongusernamethatexceeds255charactersthisuserhasawaytolongusernamethatexceeds255characters_thisuserhasawaytolongusernamethatexceeds255characters" from institution "institution-a.example.com" and fail with "maximum length for nameId exceeds configured length of 255" diff --git a/stepup/tests/behat/features/institution_configuration_api.feature b/stepup/tests/behat/features/institution_configuration_api.feature new file mode 100644 index 0000000..963f213 --- /dev/null +++ b/stepup/tests/behat/features/institution_configuration_api.feature @@ -0,0 +1,69 @@ +Feature: A management user reads and writes institution configuration in the middleware API + In order to configure institutions + As an application management user + I must be able to read/write institution configuration to the middleware API + + Scenario: Management user reads the current institution configuration before updating the FGA settings + Given I authenticate to the Middleware API + When I request "GET /management/institution-configuration" + Then the api response status code should be 200 + And institute "institution-a.example.com" has a property "use_ra" which equals 'null' + And institute "institution-a.example.com" has a property "use_raa" which equals 'null' + And institute "institution-a.example.com" has a property "select_raa" which equals 'null' + + Scenario: Management user posts a new institution configuration for institution-a.example.com + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2, + "use_ra": ["institution-a.example.com", "institution-b.example.com"], + "use_raa": ["institution-a.example.com"], + "select_raa": [] + } + } + """ + And I authenticate to the Middleware API + When I request "POST /management/institution-configuration" + Then the api response status code should be 200 + And the "status" property should equal "OK" + + Scenario: Management user reads the updated institution configuration after updating the FGA settings + Given I authenticate to the Middleware API + When I request "GET /management/institution-configuration" + Then the api response status code should be 200 + And institute "institution-a.example.com" has a property "use_ra" which equals '["institution-a.example.com","institution-b.example.com"]' + And institute "institution-a.example.com" has a property "use_raa" which equals 'null' + And institute "institution-a.example.com" has a property "select_raa" which equals '[]' + + Scenario: Management user posts an updated institution configuration for institution-a.example.com + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2, + "use_ra": [], + "select_raa": ["institution-a.example.com", "institution-b.example.com"] + } + } + """ + And I authenticate to the Middleware API + When I request "POST /management/institution-configuration" + Then the api response status code should be 200 + And the "status" property should equal "OK" + + Scenario: Management user verifies the updated institution configuration deleted the use_raa option and emptied use_ra + Given I authenticate to the Middleware API + When I request "GET /management/institution-configuration" + Then the api response status code should be 200 + And institute "institution-a.example.com" has a property "use_ra" which equals '[]' + And institute "institution-a.example.com" has a property "use_raa" which equals 'null' + And institute "institution-a.example.com" has a property "select_raa" which equals '["institution-a.example.com","institution-b.example.com"]' diff --git a/stepup/tests/behat/features/ra.feature b/stepup/tests/behat/features/ra.feature new file mode 100644 index 0000000..efb4c6b --- /dev/null +++ b/stepup/tests/behat/features/ra.feature @@ -0,0 +1,56 @@ +Feature: A RAA manages tokens tokens registered in the selfservice portal + In order to manage tokens + As a RAA + I must be able to manage second factor tokens from my institution + + Scenario: RA user can't vet a token from another institution it is not RA for + Given institution "institution-a.example.com" can "use_ra" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_ra" from institution "institution-d.example.com" + And institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-d.example.com" can "use_ra" from institution "institution-a.example.com" + And a user "Jane Toppan" identified by "urn:collab:person:institution-a.example.com:jane-a-ra" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has a vetted "yubikey" with identifier "00000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "ra" for institution "institution-a.example.com" + And a user "Joe Satriani" identified by "urn:collab:person:institution-d.example.com:joe-d1" from institution "institution-d.example.com" with UUID "00000000-0000-4000-a000-000000000002" + And the user "urn:collab:person:institution-d.example.com:joe-d1" has a verified "yubikey" with registration code "1234ABCD" + And a user "Joe Perry" identified by "urn:collab:person:institution-e.example.com:joe-e1" from institution "institution-e.example.com" with UUID "00000000-0000-4000-a000-000000000003" + And the user "urn:collab:person:institution-e.example.com:joe-e1" has a verified "yubikey" with registration code "9876WXYZ" + And a user "Jane Aone" identified by "urn:collab:person:institution-a.example.com:jane-a1" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000004" + And the user "urn:collab:person:institution-a.example.com:jane-a1" has a vetted "yubikey" with identifier "00000004" + And I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + When I search for "9876WXYZ" on the token activation page + Then I should see "Unknown activation code" + + Scenario: RA user can view the audit log of another institution identity + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + When I visit the Tokens page + And I open the audit log for a user of "institution-d.example.com" + Then I should see "institution-d.example.com" in the audit log identity overview + + Scenario: RA user can view token overview and sees tokens from other institutions + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + When I visit the Tokens page + Then I should see "institution-a.example.com" in the search results + And I should see "institution-d.example.com" in the search results + + Scenario: RA user can filter the token overview + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + When I visit the Tokens page + And I filter the "Institution" filter on "institution-d.example.com" + And I should see "institution-d.example.com" in the search results + Then I should not see "institution-a.example.com" in the search results + + Scenario: RA user can vet a token from another institution it is RA for + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + When I search for "1234ABCD" on the token activation page + Then I should see "Please connect the user's personal Yubikey with your computer" + + Scenario: SRAA user promotes "jane-a1" to be an RA + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA promotion page + Then I change the role of "Jane Aone" to become "RA" for institution "institution-a.example.com" + + Scenario: SRAA user demotes "jane-a1" to no longer be an RA + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA Management page + Then I relieve "Jane Aone" from "institution-a.example.com" of his "RA" role diff --git a/stepup/tests/behat/features/ra_candidate.feature b/stepup/tests/behat/features/ra_candidate.feature new file mode 100644 index 0000000..ba700e0 --- /dev/null +++ b/stepup/tests/behat/features/ra_candidate.feature @@ -0,0 +1,87 @@ +Feature: A RAA manages ra candidates in the ra environment + In order to promote candidates + As a RAA + I must be able to promote and demote identities + + Scenario: Provision a institution and a user to promote later on by an authorized institution + Given institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-b.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-d.example.com" can "select_raa" from institution "institution-a.example.com" + And a user "jane-a-ra" identified by "urn:collab:person:institution-a.example.com:jane-a-ra" from institution "institution-a.example.com" + And a user "jane-a1" identified by "urn:collab:person:institution-a.example.com:jane-a1" from institution "institution-a.example.com" + # The two users below are only used to create institutions for the SRAA switcher + And a user "user-b1" identified by "urn:collab:person:institution-b.example.com:user-b1" from institution "institution-b.example.com" + And a user "user-b2" identified by "urn:collab:person:institution-d.example.com:user-b2" from institution "institution-d.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has a vetted "yubikey" with identifier "00000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "raa" for institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "raa" for institution "institution-b.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "raa" for institution "institution-d.example.com" + + Scenario: SRAA user checks if "Jane Toppan" is a candidate for all institutions (without filtering) + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA promotion page + Then I should see the following candidates: + | name | institution | + | jane-a-ra | institution-a.example.com | + | jane-b1 institution-b.example.com | institution-b.example.com | + | user-b-ra institution-b.example.com | institution-b.example.com | + | user-b5 institution-b.example.com | institution-b.example.com | + | Admin | dev.openconext.local | + | SRAA2 | dev.openconext.local | + + Scenario: SRAA user checks if "Jane Toppan" is a candidate for all institutions (with filtering on institution-a) + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA promotion page + Then I should see the following candidates for "institution-a.example.com": + | name | institution | + | jane-a-ra | institution-a.example.com | + + + Scenario: SRAA user checks if "Jane Toppan" is a candidate for all institutions (with filtering on institution-b) + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA promotion page + Then I should see the following candidates for "institution-b.example.com": + | name | institution | + | jane-b1 institution-b.example.com | institution-b.example.com | + | user-b-ra institution-b.example.com | institution-b.example.com | + | user-b5 institution-b.example.com | institution-b.example.com | + + Scenario: SRAA user demotes "jane-a-ra" to no longer be an RAA for "institution-a" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA Management page + Then I relieve "jane-a-ra" from "institution-a.example.com" of his "RAA" role + + Scenario: SRAA user checks if "Jane Toppan" is a candidate for "institution-a" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA promotion page + Then I should see the following candidates for "institution-a.example.com": + | name | institution | + | jane-a-ra | institution-a.example.com | + | jane-b1 institution-b.example.com | institution-b.example.com | + | user-b-ra institution-b.example.com | institution-b.example.com | + | user-b5 institution-b.example.com | institution-b.example.com | + + Scenario: SRAA user checks if "Jane Toppan" is not a candidate for "institution-b" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA promotion page + Then I should see the following candidates for "institution-b.example.com": + | name | institution | + | jane-b1 institution-b.example.com | institution-b.example.com | + | user-b-ra institution-b.example.com | institution-b.example.com | + | user-b5 institution-b.example.com | institution-b.example.com | + + Scenario: SRAA user checks if "Jane Toppan" is not listed for "institution-a" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA Management page + Then I should see the following raas: + | name | institution | role | + | jane-a-ra | institution-b.example.com | RAA | + | jane-a-ra | institution-d.example.com | RAA | + + Scenario: SRAA user checks if "Jane Toppan" is listed for "institution-b" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA Management page + Then I should see the following raas: + | name | institution | role | + | jane-a-ra | institution-b.example.com | RAA | + | jane-a-ra | institution-d.example.com | RAA | diff --git a/stepup/tests/behat/features/ra_candidate2.feature b/stepup/tests/behat/features/ra_candidate2.feature new file mode 100644 index 0000000..126ed72 --- /dev/null +++ b/stepup/tests/behat/features/ra_candidate2.feature @@ -0,0 +1,28 @@ +Feature: A RAA manages ra candidates in the ra environment (see: https://www.pivotaltracker.com/story/show/171703175) + In order to promote candidates + As a RAA + I must be able to promote and demote identities were I'm allowed to through the authorization config + + Scenario: Provision an institution and a user to promote later on by an authorized institution + Given institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_ra" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_raa" from institution "institution-a.example.com" + + And institution "institution-d.example.com" can "select_raa" from institution "institution-a.example.com" + + And a user "joe-a-raa institution-a" identified by "urn:collab:person:institution-a.example.com:joe-a-raa" from institution "institution-a.example.com" with UUID "3af4eba5-8d1b-4da4-a6ba-c730356f36e1" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has a vetted "yubikey" identified by "00000002" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has the role "raa" for institution "institution-a.example.com" + And a user "jane-a2 institution-a" identified by "urn:collab:person:institution-a.example.com:jane-a2" from institution "institution-a.example.com" with UUID "3af4eba5-8d1b-4da4-a6ba-c730356f36e2" + And the user "urn:collab:person:institution-a.example.com:jane-a2" has a vetted "yubikey" identified by "00000003" + + And a user "jane-d-raa institution-d.nl" identified by "urn:collab:person:institution-d.example.com:jane-d-raa" from institution "institution-d.example.com" with UUID "3af4eba5-8d1b-4da4-a6ba-c730356f36e3" + And the user "urn:collab:person:institution-d.example.com:jane-d-raa" has a vetted "yubikey" identified by "00000004" + + Scenario: RAA from institution a should not see an RA(A) candidate from institution d + Given I am logged in into the ra portal as "joe-a-raa" with a "yubikey" token + When I visit the RA promotion page + Then I should see the following candidates for "institution-a.example.com": + | name | institution | + | jane-a2 institution-a | institution-a.example.com | + | joe-a-raa institution-a | institution-a.example.com | diff --git a/stepup/tests/behat/features/ra_candidate3.feature b/stepup/tests/behat/features/ra_candidate3.feature new file mode 100644 index 0000000..859afb1 --- /dev/null +++ b/stepup/tests/behat/features/ra_candidate3.feature @@ -0,0 +1,42 @@ +Feature: A RAA manages ra candidates from virtual institutions in the ra environment + In order to promote candidates from virtual institutions + As a RAA + I must be able to promote and demote identities were I'm allowed to through the authorization config + + Scenario: Provision an institution and a user to promote and demote later on by an authorized institution + Given institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_ra" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_raa" from institution "institution-a.example.com" + + And institution "institution-d.example.com" can "select_raa" from institution "institution-a.example.com" + + And a user "joe-a-raa institution-a" identified by "urn:collab:person:institution-a.example.com:joe-a-raa" from institution "institution-a.example.com" with UUID "00000000-0000-4000-8000-000000000010" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has a vetted "yubikey" identified by "00000004" + + Scenario: RAA from institution a should see "joe-a-raa" as an RA(A) candidate from "institution-d" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + And I visit the "management/create-ra/00000000-0000-4000-8000-000000000010" page in the RA environment + Then the "#ra_management_create_ra_roleAtInstitution_institution" element should contain "institution-a.example.com" + And the "#ra_management_create_ra_roleAtInstitution_institution" element should contain "institution-b.example.com" + + Scenario: SRAA user promotes "joe-a-raa" to be a RA for "institution-d" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + And I visit the RA promotion page + Then I change the role of "joe-a-raa institution-a" to become "RA" for institution "institution-d.example.com" + + Scenario: SRAA should not see "joe-a-raa" from "institution-d" as a RA(A) candidate for "institution-d" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + And I visit the "management/create-ra/00000000-0000-4000-8000-000000000010" page in the RA environment + Then the "#ra_management_create_ra_roleAtInstitution_institution" element should contain "institution-a.example.com" + And the "#ra_management_create_ra_roleAtInstitution_institution" element should not contain "institution-c.example.com" + + Scenario: SRAA user demotes "joe-a-raa" from a RA of "institution-d" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + And I visit the RA Management page + Then I relieve "joe-a-raa institution-a" from "institution-d.example.com" of his "RA" role + + Scenario: RAA from institution a should see "joe-a-raa" again as an RA(A) candidate from "institution-d" + Given I am logged in into the ra portal as "admin" with a "yubikey" token + And I visit the "management/create-ra/00000000-0000-4000-8000-000000000010" page in the RA environment + Then the "#ra_management_create_ra_roleAtInstitution_institution" element should contain "institution-a.example.com" + And the "#ra_management_create_ra_roleAtInstitution_institution" element should contain "institution-d.example.com" diff --git a/stepup/tests/behat/features/ra_export.feature b/stepup/tests/behat/features/ra_export.feature new file mode 100644 index 0000000..14c5da2 --- /dev/null +++ b/stepup/tests/behat/features/ra_export.feature @@ -0,0 +1,44 @@ +Feature: A RAA can export tokens registered in the selfservice portal + In order to export tokens + As a RAA + I must be able to export second factor tokens + + Scenario: RA user can't vet a token from another institution it is not RA for + Given a user "user-a-ra" identified by "urn:collab:person:institution-a.example.com:user-a-ra" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000001" + And a user "jane-a-raa" identified by "urn:collab:person:institution-a.example.com:jane-a-raa" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000002" + And a user "joe-a-raa" identified by "urn:collab:person:institution-a.example.com:joe-a-raa" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000003" + And institution "institution-a.example.com" can "use_ra" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_raa" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-b.example.com" can "use_raa" from institution "institution-b.example.com" + And institution "institution-b.example.com" can "select_raa" from institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:user-a-ra" has a vetted "yubikey" with identifier "00000001" + And the user "urn:collab:person:institution-a.example.com:user-a-ra" has the role "ra" for institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a-raa" has a vetted "yubikey" with identifier "00000002" + And the user "urn:collab:person:institution-a.example.com:jane-a-raa" has the role "raa" for institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has a vetted "yubikey" with identifier "00000003" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has the role "ra" for institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has the role "raa" for institution "institution-b.example.com" + + Scenario: an RA user can not export tokens + Given I am logged in into the ra portal as "user-a-ra" with a "yubikey" token + When I visit the Tokens page + Then I should not see a token export button + + Scenario: an RAA user can export tokens + Given I am logged in into the ra portal as "jane-a-raa" with a "yubikey" token + When I visit the Tokens page + And I click on the token export button + Then the response should contain "\"Token ID\",Type,Name,Email,Institution,\"Document Number\",Status" + And the response should contain "00000003,yubikey,joe-a-raa,foo@bar.com,institution-a.example.com,123456,vetted" + And the response should contain "00000002,yubikey,jane-a-raa,foo@bar.com,institution-a.example.com,123456,vetted" + And the response should contain "00000001,yubikey,user-a-ra,foo@bar.com,institution-a.example.com,123456,vetted" + + Scenario: a user which is at least RAA for one institution can export tokens + Given I am logged in into the ra portal as "joe-a-raa" with a "yubikey" token + When I visit the Tokens page + And I click on the token export button + Then the response should contain "\"Token ID\",Type,Name,Email,Institution,\"Document Number\",Status" + And the response should contain "00000003,yubikey,joe-a-raa,foo@bar.com,institution-a.example.com,123456,vetted" + And the response should contain "00000002,yubikey,jane-a-raa,foo@bar.com,institution-a.example.com,123456,vetted" + And the response should contain "00000001,yubikey,user-a-ra,foo@bar.com,institution-a.example.com,123456,vetted" diff --git a/stepup/tests/behat/features/ra_grants.feature b/stepup/tests/behat/features/ra_grants.feature new file mode 100644 index 0000000..0d53284 --- /dev/null +++ b/stepup/tests/behat/features/ra_grants.feature @@ -0,0 +1,175 @@ +Feature: A RA(A) should only have access to certain pages + + Scenario: Provision an institution and a user to promote later on by an authorized institution + Given institution "dev.openconext.local" can "select_raa" from institution "dev.openconext.local" + And institution "dev.openconext.local" can "use_raa" from institution "dev.openconext.local" + And institution "institution-a.example.com" can "use_ra" from institution "dev.openconext.local" + And institution "institution-b.example.com" can "use_raa" from institution "dev.openconext.local" + And institution "institution-d.example.com" can "select_raa" from institution "institution-d.example.com" + And institution "institution-d.example.com" can "use_raa" from institution "institution-d.example.com" + And a user "RA institution A" identified by "urn:collab:person:dev.openconext.local:joe--ra" from institution "dev.openconext.local" with UUID "00000000-0000-4000-a000-000000000001" + And a user "RAA institution B" identified by "urn:collab:person:dev.openconext.local:joe--raa" from institution "dev.openconext.local" with UUID "00000000-0000-4000-a000-000000000002" + And a user "RAA institution D" identified by "urn:collab:person:institution-d.example.com:joe-d-raa" from institution "institution-d.example.com" with UUID "00000000-0000-4000-a000-000000000003" + And a user "RA(A) candidate" identified by "urn:collab:person:institution-d.example.com:joe--candidate" from institution "dev.openconext.local" with UUID "00000000-0000-4000-a000-000000000004" + And the user "urn:collab:person:dev.openconext.local:joe--ra" has a vetted "yubikey" with identifier "00000001" + And the user "urn:collab:person:dev.openconext.local:joe--raa" has a vetted "yubikey" with identifier "00000002" + And the user "urn:collab:person:institution-d.example.com:joe-d-raa" has a vetted "yubikey" with identifier "00000003" + And the user "urn:collab:person:institution-d.example.com:joe--candidate" has a vetted "yubikey" with identifier "00000004" + And the user "urn:collab:person:dev.openconext.local:joe--ra" has the role "ra" for institution "dev.openconext.local" + And the user "urn:collab:person:dev.openconext.local:joe--raa" has the role "raa" for institution "dev.openconext.local" + And the user "urn:collab:person:institution-d.example.com:joe-d-raa" has the role "raa" for institution "institution-d.example.com" + + # Token page + Scenario: An anonymous user can not view the tokens page + Given I visit the "second-factors" page in the RA environment + Then I should see "Enter your username and password" + + Scenario: RA can view the tokens page + Given I am logged in into the ra portal as "joe--ra" with a "yubikey" token + When I visit the "second-factors" page in the RA environment + Then the response status code should be 200 + + Scenario: RAA can view the tokens page + Given I am logged in into the ra portal as "joe--raa" with a "yubikey" token + When I visit the "second-factors" page in the RA environment + Then the response status code should be 200 + + Scenario: RAA from other institution can view the tokens page + Given I am logged in into the ra portal as "joe-d-raa" with a "yubikey" token + When I visit the "second-factors" page in the RA environment + Then the response status code should be 200 + + Scenario: SRAA can view the tokens page + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the "second-factors" page in the RA environment + Then the response status code should be 200 + + + # RA-management page + Scenario: An anonymous user can not view the ra-management page + Given I visit the "management/ra" page in the RA environment + Then I should see "Enter your username and password" + + Scenario: RA can not view the ra-management page + Given I am logged in into the ra portal as "joe--ra" with a "yubikey" token + When I visit the "management/ra" page in the RA environment + Then the response status code should be 403 + + Scenario: RAA can view the ra-management page + Given I am logged in into the ra portal as "joe--raa" with a "yubikey" token + When I visit the "management/ra" page in the RA environment + Then the response status code should be 200 + + Scenario: RAA from another institution can not view the ra-management page + Given I am logged in into the ra portal as "joe-d-raa" with a "yubikey" token + When I visit the "management/ra" page in the RA environment + Then the response status code should be 200 + + Scenario: SRAA can view the ra-management page + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the "management/ra" page in the RA environment + Then the response status code should be 200 + + + # RA-management create page + Scenario: An anonymous user can not view the ra-management create page + Given I visit the "management/create-ra/00000000-0000-4000-a000-000000000004" page in the RA environment + Then I should see "Enter your username and password" + + Scenario: RA can not view the ra-management create page + Given I am logged in into the ra portal as "joe--ra" with a "yubikey" token + When I visit the "management/create-ra/00000000-0000-4000-a000-000000000004" page in the RA environment + Then the response status code should be 403 + + Scenario: RAA can view the ra-management create page + Given I am logged in into the ra portal as "joe--raa" with a "yubikey" token + When I visit the "management/create-ra/00000000-0000-4000-a000-000000000004" page in the RA environment + Then the response status code should be 200 + + Scenario: RAA from another institution can not view the ra-management create page + Given I am logged in into the ra portal as "joe-d-raa" with a "yubikey" token + When I visit the "management/create-ra/00000000-0000-4000-a000-000000000004" page in the RA environment + Then the response status code should be 404 + + Scenario: SRAA can view the ra-management create page + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the "management/create-ra/00000000-0000-4000-a000-000000000004" page in the RA environment + Then the response status code should be 200 + + + # RA-management amend page + Scenario: An anonymous user can not view the ra-management amend page + Given I visit the "management/amend-ra-information/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then I should see "Enter your username and password" + + Scenario: RA can not view the ra-management amend page + Given I am logged in into the ra portal as "joe--ra" with a "yubikey" token + When I visit the "management/amend-ra-information/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then the response status code should be 403 + + Scenario: RAA can view the ra-management amend page + Given I am logged in into the ra portal as "joe--raa" with a "yubikey" token + When I visit the "management/amend-ra-information/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then the response status code should be 200 + + Scenario: RAA from institution-c can not view the ra-management amend page + Given I am logged in into the ra portal as "joe-d-raa" with a "yubikey" token + When I visit the "management/amend-ra-information/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then the response status code should be 404 + + Scenario: SRAA can view the ra-management amend page + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the "management/amend-ra-information/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then the response status code should be 200 + + + # RA-management retract page + Scenario: An anonymous user can not view the ra-management retract page + Given I visit the "management/retract-registration-authority/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then I should see "Enter your username and password" + + Scenario: RA can not view the ra-management retract page + Given I am logged in into the ra portal as "joe--ra" with a "yubikey" token + When I visit the "management/retract-registration-authority/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then the response status code should be 403 + + Scenario: RAA can view the ra-management retract page + Given I am logged in into the ra portal as "joe--raa" with a "yubikey" token + When I visit the "management/retract-registration-authority/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then the response status code should be 200 + + Scenario: RAA from another institution can not view the ra-management retract page + Given I am logged in into the ra portal as "joe-d-raa" with a "yubikey" token + When I visit the "management/retract-registration-authority/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then the response status code should be 404 + + Scenario: SRAA can view the ra-management retract page + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the "management/retract-registration-authority/00000000-0000-4000-a000-000000000001/dev.openconext.local" page in the RA environment + Then the response status code should be 200 + + + # RA-candidate page + Scenario: An anonymous user can not view the ra-candidate page + Given I visit the "management/search-ra-candidate" page in the RA environment + Then I should see "Enter your username and password" + + Scenario: RA can not view the ra-candidate page + Given I am logged in into the ra portal as "joe--ra" with a "yubikey" token + When I visit the "management/search-ra-candidate" page in the RA environment + Then the response status code should be 403 + + Scenario: RAA can view the ra-candidate page + Given I am logged in into the ra portal as "joe--raa" with a "yubikey" token + When I visit the "management/search-ra-candidate" page in the RA environment + Then the response status code should be 200 + + Scenario: RAA from another institution can view the ra-candidate page + Given I am logged in into the ra portal as "joe--raa" with a "yubikey" token + When I visit the "management/search-ra-candidate" page in the RA environment + Then the response status code should be 200 + + Scenario: SRAA can view the ra-candidate page + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the "management/search-ra-candidate" page in the RA environment + Then the response status code should be 200 diff --git a/stepup/tests/behat/features/ra_insitution-configuration.feature b/stepup/tests/behat/features/ra_insitution-configuration.feature new file mode 100644 index 0000000..e2fa354 --- /dev/null +++ b/stepup/tests/behat/features/ra_insitution-configuration.feature @@ -0,0 +1,66 @@ +Feature: A RAA can view the institution configuration + + Scenario: Jane Toppan is RAA at Institution A + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": true, + "sso_on_2fa": true, + "allow_self_asserted_tokens": true, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2 + }, + "institution-d.example.com": { + "use_ra_locations": false, + "show_raa_contact_information": false, + "verify_email": false, + "allowed_second_factors": ["sms"], + "number_of_tokens_per_identity": 1, + "use_raa": [ + "institution-a.example.com" + ] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + And a user "Jane Toppan" identified by "urn:collab:person:institution-a.example.com:jane-a-raa" from institution "institution-a.example.com" with UUID "00000000-0000-4000-A000-000000000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-raa" has a vetted "yubikey" with identifier "00000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-raa" has the role "raa" for institution "institution-a.example.com" + + Scenario: RAA user for institution A sees the institution-configuration for that institution + Given I am logged in into the ra portal as "jane-a-raa" with a "yubikey" token + When I visit the "institution-configuration" page in the RA environment + Then I should see "Configuration of institution-a.example.com" + And The institution configuration should be: + | Label | Value | + | Use RA locations enabled? | Yes | + | Show RAA contact information? | Yes | + | E-mail verification enabled? | Yes | + | Single sign on on second factor authentications? | Yes | + | Token activation using an activated token | Allowed | + | Activate a token without the service desk or an activated token | Allowed | + | Allowed second factor tokens | All enabled tokens are available | + | Number of tokens per identity | 2 | + | From which institution(s) can users be assigned the RA(A) role for this institution? | institution-a.example.com | + | From which institution(s) are the RAs an RA for this institution? | institution-a.example.com | + | From which institution(s) are the RAAs an RAA for this institution? | institution-a.example.com | + Then I switch to institution "institution-d.example.com" with institution switcher + Then I should see "Configuration of institution-d.example.com" + And The institution configuration should be: + | Label | Value | + | Use RA locations enabled? | No | + | Show RAA contact information? | No | + | E-mail verification enabled? | No | + | Single sign on on second factor authentications? | No | + | Token activation using an activated token | Not allowed | + | Activate a token without the service desk or an activated token | Not allowed | + | Allowed second factor tokens | sms | + | Number of tokens per identity | 1 | + | From which institution(s) can users be assigned the RA(A) role for this institution? | institution-d.example.com | + | From which institution(s) are the RAs an RA for this institution? | institution-d.example.com | + | From which institution(s) are the RAAs an RAA for this institution? | institution-a.example.com | diff --git a/stepup/tests/behat/features/ra_locations.feature b/stepup/tests/behat/features/ra_locations.feature new file mode 100644 index 0000000..d97c362 --- /dev/null +++ b/stepup/tests/behat/features/ra_locations.feature @@ -0,0 +1,81 @@ +Feature: A RAA can view the institution configuration + + Scenario: Jane Toppan is RAA at Institution A + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2 + }, + "institution-d.example.com": { + "use_ra_locations": false, + "show_raa_contact_information": false, + "verify_email": false, + "allowed_second_factors": ["sms"], + "number_of_tokens_per_identity": 1, + "use_raa": [ + "institution-a.example.com" + ] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + And a user "Jane Toppan" identified by "urn:collab:person:institution-a.example.com:jane-a-raa" from institution "institution-a.example.com" with UUID "00000000-0000-4000-8000-000000000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-raa" has a vetted "yubikey" with identifier "00000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-raa" has the role "raa" for institution "institution-a.example.com" + + Scenario: A user sees the locations of institutions it's RAA for + Given I am logged in into the ra portal as "jane-a-raa" with a "yubikey" token + When I visit the "locations" page in the RA environment + Then I should see "Locations of institution-a.example.com" + And I should see "No RA locations found for the current institution." + Then I switch to institution "institution-d.example.com" with institution switcher + And I should see "Locations of institution-d.example.com" + And I should see "No RA locations found for the current institution." + + Scenario: A user can add locations of an institution it's RAA for + Given I am logged in into the ra portal as "jane-a-raa" with a "yubikey" token + When I visit the "locations" page in the RA environment + And I switch to institution "institution-d.example.com" with institution switcher + And I should see "Locations of institution-d.example.com" + And I should see "No RA locations found for the current institution." + And I follow "Add an RA Location" + And I fill in the following: + | ra_create_ra_location_name | The name of the test location for institution D | + | ra_create_ra_location_location | The location itself for institution D | + | ra_create_ra_location_contactInformation | An address for the test location of institution D | + And I press "ra_create_ra_location_button-group_create_ra_location" + Then I should see "Locations of institution-d.example.com" + And I should see "The name of the test location for institution D" + And I should see "The location itself for institution D" + And I should see "An address for the test location of institution D" + Then I switch to institution "institution-a.example.com" with institution switcher + And I should see "Locations of institution-a.example.com" + And I should see "No RA locations found for the current institution." + + Scenario: A user can edit the added location of an institution it's RAA for + Given I am logged in into the ra portal as "jane-a-raa" with a "yubikey" token + When I visit the "locations" page in the RA environment + And I switch to institution "institution-d.example.com" with institution switcher + And I should see "Locations of institution-d.example.com" + And I should see "The name of the test location for institution D" + And I follow "Edit" + And I fill in the following: + | ra_change_ra_location_name | The name of the test location for institution D, updated! | + | ra_change_ra_location_location | The location itself for institution D, updated! | + | ra_change_ra_location_contactInformation | An address for the test location of institution D, updated! | + And I press "ra_change_ra_location_button-group_change_ra_location" + Then I should see "Locations of institution-d.example.com" + And I should see "The name of the test location for institution D, updated!" + And I should see "The location itself for institution D, updated!" + And I should see "An address for the test location of institution D, updated!" + Then I switch to institution "institution-a.example.com" with institution switcher + And I should see "Locations of institution-a.example.com" + And I should see "No RA locations found for the current institution." + + # TODO: test delete endpoint and confirmation modal diff --git a/stepup/tests/behat/features/ra_login-exception.feature b/stepup/tests/behat/features/ra_login-exception.feature new file mode 100644 index 0000000..032c586 --- /dev/null +++ b/stepup/tests/behat/features/ra_login-exception.feature @@ -0,0 +1,14 @@ +Feature: A RAA can only manage R RA(A)'s on the promotion page + In order to manage RA(A)'s + As a user + I must see a sane error message when I login to RA but I'm not accredited as RA + + Scenario: Provision a institution and a user to promote later on by an authorized institution + Given a user "joe-a-raa" identified by "urn:collab:person:institution-a.example.com:joe-a-raa" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000001" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has a vetted "yubikey" with identifier "00000004" + + Scenario: User "joe-a-raa" tries to login while not accredited as RAA should be informed + Given I try to login into the ra portal as "joe-a-raa" with a "yubikey" token + Then I should see "Error - Access denied" + And I should see "Authentication was successful, but you are not authorised to use the RA management portal" + diff --git a/stepup/tests/behat/features/ra_multiple_tokens.feature b/stepup/tests/behat/features/ra_multiple_tokens.feature new file mode 100644 index 0000000..b2deea6 --- /dev/null +++ b/stepup/tests/behat/features/ra_multiple_tokens.feature @@ -0,0 +1,102 @@ +Feature: A RAA (jane a ra) has two loa 3 tokens which makes her a valid RA candidate + In order to verify you will lose ra credibility + As an Admin + I verify removing all tokes of jane makes her lose her RA roles + + Scenario: Provision a institution and a user to promote later on by an authorized institution + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2, + "select_raa": [ + "institution-a.example.com" + ] + }, + "institution-b.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2, + "select_raa": [ + "institution-a.example.com" + ] + }, + "institution-d.example.com": { + "use_ra_locations": false, + "show_raa_contact_information": false, + "verify_email": false, + "allowed_second_factors": ["sms"], + "number_of_tokens_per_identity": 1, + "select_raa": [ + "institution-a.example.com" + ] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + And a user "jane-a-ra" identified by "urn:collab:person:institution-a.example.com:jane-a-ra" from institution "institution-a.example.com" with UUID "00000000-0000-4000-8000-000000000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has a vetted "yubikey" with identifier "00000004" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has a vetted "demo-gssp" with identifier "gssp-identifier123" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "raa" for institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "raa" for institution "institution-b.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "raa" for institution "institution-d.example.com" + + Scenario: SRAA user checks if "Jane Toppan" is not a candidate for institutions + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA promotion page + Then I should see the following candidates: + | name | institution | + | jane-a-ra | institution-a.example.com | + | jane-b1 institution-b.example.com | institution-b.example.com | + | user-b5 institution-b.example.com | institution-b.example.com | + | user-b-ra institution-b.example.com | institution-b.example.com | + | Admin | dev.openconext.local | + | SRAA2 | dev.openconext.local | + + Scenario: SRAA user checks if "jane-a-ra" is a candidate for institutions if relieved from the RAA role + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the RA Management page + And I relieve "jane-a-ra" from "institution-a.example.com" of his "RAA" role + Then I visit the RA promotion page + And I should see the following candidates: + | name | institution | + | jane-a-ra | institution-a.example.com | + | jane-b1 institution-b.example.com | institution-b.example.com | + | user-b5 institution-b.example.com | institution-b.example.com | + | user-b-ra institution-b.example.com | institution-b.example.com | + | Admin | dev.openconext.local | + | SRAA2 | dev.openconext.local | + + Scenario: Sraa revokes only one vetted token from "jane-a-ra" and that shouldn't remove her as candidate + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the Tokens page + And I remove token with identifier "00000004" from user "jane-a-ra" + Then I visit the RA promotion page + And I should see the following candidates: + | name | institution | + | jane-a-ra | institution-a.example.com | + | jane-b1 institution-b.example.com | institution-b.example.com | + | user-b5 institution-b.example.com | institution-b.example.com | + | user-b-ra institution-b.example.com | institution-b.example.com | + | Admin | dev.openconext.local | + | SRAA2 | dev.openconext.local | + + Scenario: Sraa revokes the last vetted token from "Jane Toppan" and that must remove her as candidate + Given I am logged in into the ra portal as "admin" with a "yubikey" token + When I visit the Tokens page + And I remove token with identifier "gssp-identifier123" from user "jane-a-ra" + Then I visit the RA promotion page + And I should see the following candidates: + | name | institution | + | jane-b1 institution-b.example.com | institution-b.example.com | + | user-b5 institution-b.example.com | institution-b.example.com | + | user-b-ra institution-b.example.com | institution-b.example.com | + | Admin | dev.openconext.local | + | SRAA2 | dev.openconext.local | diff --git a/stepup/tests/behat/features/ra_profile.feature b/stepup/tests/behat/features/ra_profile.feature new file mode 100644 index 0000000..6caa620 --- /dev/null +++ b/stepup/tests/behat/features/ra_profile.feature @@ -0,0 +1,96 @@ +Feature: A RA(A) can view profile information + + Scenario: Jane Toppan is RAA at Institution A and Joe is RA at Insittution A + Given I have the payload + """ + { + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 2, + "select_raa": [ + "institution-a.example.com" + ] + }, + "institution-f.example.com": { + "use_ra_locations": false, + "show_raa_contact_information": false, + "verify_email": false, + "allowed_second_factors": ["sms"], + "number_of_tokens_per_identity": 1, + "select_raa": [] + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + And institution "institution-a.example.com" can "use_ra" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_raa" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And a user "Jane Toppan" identified by "urn:collab:person:institution-a.example.com:jane-a-ra" from institution "institution-a.example.com" with UUID "00000000-0000-4000-A000-000000000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has a vetted "yubikey" identified by "00000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "raa" for institution "institution-a.example.com" + And a user "Joe Satriani" identified by "urn:collab:person:institution-a.example.com:joe-a-ra" from institution "institution-a.example.com" with UUID "00000000-0000-4000-A000-000000000002" + And the user "urn:collab:person:institution-a.example.com:joe-a-ra" has a vetted "yubikey" identified by "00000002" + And the user "urn:collab:person:institution-a.example.com:joe-a-ra" has the role "ra" for institution "institution-a.example.com" + + Scenario: RAA user for one institution sees the authorization for that institution + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + When I visit the "profile" page in the RA environment + Then I should see the following profile: + | Label | Value | + | Name | Jane Toppan | + | Username (NameID) | urn:collab:person:institution-a.example.com:jane-a-ra | + | E-mail | foo@bar.com | + | Preferred locale | en_GB | + | Authorizations | RAA @ institution-a.example.com | + + Scenario: RAA user for multiple institutions sees the implicit authorizations + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + And institution "institution-b.example.com" can "use_ra" from institution "institution-a.example.com" + When I visit the "profile" page in the RA environment + Then I should see the following profile: + | Label | Value | + | Name | Jane Toppan | + | Username (NameID) | urn:collab:person:institution-a.example.com:jane-a-ra | + | E-mail | foo@bar.com | + | Preferred locale | en_GB | + | Authorizations | RAA @ institution-a.example.com RA @ institution-b.example.com | + + Scenario: RAA user is accredited correct RAA role at institution B + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + And institution "institution-b.example.com" can "use_raa" from institution "institution-a.example.com" + When I visit the "profile" page in the RA environment + Then I should see the following profile: + | Label | Value | + | Name | Jane Toppan | + | Username (NameID) | urn:collab:person:institution-a.example.com:jane-a-ra | + | E-mail | foo@bar.com | + | Preferred locale | en_GB | + | Authorizations | RAA @ institution-a.example.com RAA @ institution-b.example.com | + + Scenario: RA user for multiple institutions sees the implicit authorizations + Given I am logged in into the ra portal as "joe-a-ra" with a "yubikey" token + And institution "institution-b.example.com" can "use_ra" from institution "institution-a.example.com" + When I visit the "profile" page in the RA environment + Then I should see the following profile: + | Label | Value | + | Name | Joe Satriani | + | Username (NameID) | urn:collab:person:institution-a.example.com:joe-a-ra | + | E-mail | foo@bar.com | + | Preferred locale | en_GB | + | Authorizations | RA @ institution-a.example.com RA @ institution-b.example.com | + + Scenario: RA user is not accredited RAA role at institution B + Given I am logged in into the ra portal as "joe-a-ra" with a "yubikey" token + And institution "institution-b.example.com" can "use_raa" from institution "institution-a.example.com" + When I visit the "profile" page in the RA environment + Then I should see the following profile: + | Label | Value | + | Name | Joe Satriani | + | Username (NameID) | urn:collab:person:institution-a.example.com:joe-a-ra | + | E-mail | foo@bar.com | + | Preferred locale | en_GB | + | Authorizations | RA @ institution-a.example.com | diff --git a/stepup/tests/behat/features/ra_select-raa.feature b/stepup/tests/behat/features/ra_select-raa.feature new file mode 100644 index 0000000..ac7c0f9 --- /dev/null +++ b/stepup/tests/behat/features/ra_select-raa.feature @@ -0,0 +1,35 @@ +Feature: A RAA manages RA(A)'s on the promotion page + In order to manage RA(A)'s + As a RAA + I must be able to promote and demote identities to RA(A)'s + + Scenario: Provision a institution and a user to promote later on by an authorized institution + Given a user "jane-a2" identified by "urn:collab:person:institution-a.example.com:jane-a2" from institution "institution-a.example.com" + Given a user "joe-d1" identified by "urn:collab:person:institution-d.example.com:joe-d1" from institution "institution-d.example.com" + And the user "urn:collab:person:institution-a.example.com:jane-a2" has a vetted "yubikey" identified by "00000001" + And the user "urn:collab:person:institution-d.example.com:joe-d1" has a vetted "yubikey" identified by "00000005" + And institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "select_raa" from institution "institution-d.example.com" + And institution "institution-d.example.com" can "use_raa" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_raa" from institution "institution-a.example.com" + And institution "institution-d.example.com" can "use_ra" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_ra" from institution "institution-a.example.com" + Scenario: SRAA user promotes "jane-a2" to be an RAA + Given I am logged in into the ra portal as "admin" with a "yubikey" token + And I visit the RA promotion page + Then I change the role of "jane-a2" to become "RAA" for institution "institution-a.example.com" + + Scenario: User "jane-a2" promotes "joe-d1" to be an RA + Given I am logged in into the ra portal as "jane-a2" with a "yubikey" token + And I visit the RA promotion page + Then I change the role of "joe-d1" to become "RA" for institution "institution-a.example.com" + + Scenario: User "jane-a2" demotes "joe-d1" to no longer be an RA + Given I am logged in into the ra portal as "jane-a2" with a "yubikey" token + And I visit the RA Management page + Then I relieve "joe-d1" from "institution-a.example.com" of his "RA" role + + Scenario: SRAA user demotes "jane-a2" to no longer be an RAA + Given I am logged in into the ra portal as "admin" with a "yubikey" token + And I visit the RA Management page + Then I relieve "jane-a2" from "institution-a.example.com" of his "RAA" role diff --git a/stepup/tests/behat/features/ra_select-raa_use_raa.feature b/stepup/tests/behat/features/ra_select-raa_use_raa.feature new file mode 100644 index 0000000..bf0e2f9 --- /dev/null +++ b/stepup/tests/behat/features/ra_select-raa_use_raa.feature @@ -0,0 +1,28 @@ +Feature: A RAA can only manage R RA(A)'s on the promotion page + In order to manage RA(A)'s + As a RAA + I must only be able to manage RA(A)'s if select_raa is set but also use_raa is explicitly set + + Scenario: Provision an institution and a user to promote later on by an authorized institution without "institution-d" having "select_raa" rights from "instituition-d" + Given a user "joe-a-raa" identified by "urn:collab:person:institution-a.example.com:joe-a-raa" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000001" + And the user "urn:collab:person:institution-a.example.com:joe-a-raa" has a vetted "yubikey" with identifier "00000010" + And a user "jane-d-user" identified by "urn:collab:person:institution-d.example.com:jane-d-user" from institution "institution-d.example.com" with UUID "00000000-0000-4000-a000-000000000002" + And the user "urn:collab:person:institution-d.example.com:jane-d-user" has a vetted "yubikey" with identifier "183928174" + And institution "institution-a.example.com" can "use_raa" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "use_raa" from institution "institution-d.example.com" + And institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "select_raa" from institution "institution-d.example.com" + And institution "institution-d.example.com" can "use_raa" from institution "institution-a.example.com" + And institution "institution-d.example.com" can "use_raa" from institution "institution-d.example.com" + And institution "institution-d.example.com" can "select_raa" from institution "institution-a.example.com" + + Scenario: SRAA user promotes "joe-a-raa" to be an RAA + Given I am logged in into the ra portal as "admin" with a "yubikey" token + And I visit the RA promotion page + Then I change the role of "joe-a-raa" to become "RAA" for institution "institution-a.example.com" + + Scenario: User "joe-a-raa" can only make "jane-d-user" to be an RAA for institution-a + Given I am logged in into the ra portal as "joe-a-raa" with a "yubikey" token + And I visit the "management/create-ra/00000000-0000-4000-a000-000000000002" page in the RA environment + Then the "#ra_management_create_ra_roleAtInstitution_institution" element should not contain "institution-d.example.com" + diff --git a/stepup/tests/behat/features/ra_vet.feature b/stepup/tests/behat/features/ra_vet.feature new file mode 100644 index 0000000..c82fa5c --- /dev/null +++ b/stepup/tests/behat/features/ra_vet.feature @@ -0,0 +1,30 @@ +Feature: A RA manages tokens tokens registered in the selfservice portal + In order to manage tokens + As a RA + I must be able to manage second factor tokens in RA + + Scenario: Provision an institution and a user to promote later on by an authorized institution + Given institution "institution-a.example.com" can "use_ra" from institution "institution-a.example.com" + And institution "institution-a.example.com" can "select_raa" from institution "institution-a.example.com" + And institution "institution-d.example.com" can "use_ra" from institution "institution-a.example.com" + And a user "Jane Toppan" identified by "urn:collab:person:institution-a.example.com:jane-a-ra" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has a vetted "yubikey" with identifier "00000001" + And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has the role "ra" for institution "institution-a.example.com" + + Scenario: RA user can vet a token from an institution it is RA for + Given I am logged in into the selfservice portal as "joe-a1" + And I register a new SMS token + And I verify my e-mail address and choose the "RA vetting" vetting type + When I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + And I vet the last added second factor + + Scenario: RA user can view the audit log of an institution identity + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + When I visit the Tokens page + And I open the audit log for a user of "institution-a.example.com" + Then I should see "institution-a.example.com" in the audit log identity overview + + Scenario: RA user can remove the token of an identity + Given I am logged in into the ra portal as "jane-a-ra" with a "yubikey" token + When I visit the Tokens page + And I remove token with identifier "+31 (0) 612345678" from user "joe-a1 institution-a.example.com" diff --git a/stepup/tests/behat/features/self_vet.feature b/stepup/tests/behat/features/self_vet.feature new file mode 100644 index 0000000..3619779 --- /dev/null +++ b/stepup/tests/behat/features/self_vet.feature @@ -0,0 +1,47 @@ +Feature: A user manages his tokens in the selfservice portal + In order to use a self vetted second factor token + As a user + I must be able to manage my second factor tokens + + Scenario: Setup of the institution configuration and test users + Given I have the payload + """ + { + "dev.openconext.local": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 3 + }, + "institution-a.example.com": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": true, + "allowed_second_factors": [], + "number_of_tokens_per_identity": 3 + } + } + """ + And I authenticate to the Middleware API + And I request "POST /management/institution-configuration" + + Scenario: A user self vets a token in selfservice + Given a user "joe-a2" identified by "urn:collab:person:institution-a.example.com:joe-a2" from institution "institution-a.example.com" with UUID "00000000-0000-4000-a000-000000000001" + And the user "urn:collab:person:institution-a.example.com:joe-a2" has a vetted "yubikey" with identifier "00000001" + And I am logged in into the selfservice portal as "joe-a2" + And I self-vet a new SMS token with my Yubikey token + And I visit the "overview" page in the selfservice portal + Then I should see "The following tokens are registered for your account." + And I should see "SMS" + And I should see "Yubikey" + + Scenario: A user needs a suitable token to self vet + Given a user "joe-a3" identified by "urn:collab:person:institution-a.example.com:joe-a3" from institution "institution-a.example.com" + And the user "urn:collab:person:institution-a.example.com:joe-a3" has a vetted "sms" with identifier "+31 (0) 612345678" + And I am logged in into the selfservice portal as "joe-a3" + And I try to self-vet a new Yubikey token with my SMS token + # The self vet option is not available on the token vetting page + Then I should not see "Use your existing token" diff --git a/stepup/tests/behat/features/selfservice.feature b/stepup/tests/behat/features/selfservice.feature new file mode 100644 index 0000000..fcb99ed --- /dev/null +++ b/stepup/tests/behat/features/selfservice.feature @@ -0,0 +1,35 @@ +Feature: A user manages his tokens in the SelfService portal + In order to use a second factor token + As a user + I must be able to manage my second factor tokens + + Scenario: A user registers a SMS token in selfservice using RA vetting + Given I am logged in into the selfservice portal as "joe-a1" + When I register a new "SMS" token + And I verify my e-mail address and choose the "RA vetting" vetting type + And I vet my "SMS" second factor at the information desk + + Scenario: A user registers a Yubikey token in selfservice using RA vetting + Given I am logged in into the selfservice portal as "joe-a2" + When I register a new "Yubikey" token + And I verify my e-mail address and choose the "RA vetting" vetting type + And I vet my "Yubikey" second factor at the information desk + + Scenario: A user registers a Demo GSSP token in selfservice using RA vetting + Given I am logged in into the selfservice portal as "joe-a3" + When I register a new "Demo GSSP" token + And I verify my e-mail address and choose the "RA vetting" vetting type + And I vet my "Demo GSSP" second factor at the information desk + + Scenario: A user registers a SMS token in selfservice using RA vetting without mail verification + Given I am logged in into the selfservice portal as "joe-b1" + When I register a new "Yubikey" token + And I choose the "RA vetting" vetting type + And I vet my "Yubikey" second factor at the information desk + + Scenario: After token registration, the token can be viewed on the token overview page + Given I am logged in into the selfservice portal as "joe-a1" + Then I visit the "overview" page in the selfservice portal + Then I should see "The following tokens are registered for your account." + And I should see "SMS" + And I should see "Test a token" diff --git a/stepup/tests/behat/features/selfservice_sat.feature b/stepup/tests/behat/features/selfservice_sat.feature new file mode 100644 index 0000000..2631611 --- /dev/null +++ b/stepup/tests/behat/features/selfservice_sat.feature @@ -0,0 +1,33 @@ +Feature: A user manages his tokens in the SelfService portal + In order to SAT register a second factor token + As a user + I must be able to manage my second factor tokens + + Scenario: A user registers a SMS token in selfservice using SAT + Given I am logged in into the selfservice portal as "user-a1" + When I register a new "SMS" token + And I verify my e-mail address and choose the "Self Asserted Token registration" vetting type + And I vet my "SMS" second factor in selfservice + And "1" recovery tokens are activated + + Scenario: A user registers a Yubikey token in selfservice using SAT + Given I am logged in into the selfservice portal as "user-a2" + When I register a new "Yubikey" token + And I verify my e-mail address and choose the "Self Asserted Token registration" vetting type + And I vet my "Yubikey" second factor in selfservice + And "1" recovery tokens are activated + + Scenario: A user registers a Demo GSSP token in selfservice using SAT + Given I am logged in into the selfservice portal as "user-a3" + When I register a new "Demo GSSP" token + And I verify my e-mail address and choose the "Self Asserted Token registration" vetting type + And I vet my "Demo GSSP" second factor in selfservice + And "1" recovery tokens are activated + + Scenario: A user can register an additional recovery token + Given I am logged in into the selfservice portal as "user-a4" + When I register a new "Yubikey" token + And I verify my e-mail address and choose the "Self Asserted Token registration" vetting type + And I vet my "Yubikey" second factor in selfservice + Then I can add an "SMS" recovery token using "Yubikey" + And "2" recovery tokens are activated diff --git a/stepup/tests/behat/features/sfo.feature b/stepup/tests/behat/features/sfo.feature new file mode 100644 index 0000000..0104c3b --- /dev/null +++ b/stepup/tests/behat/features/sfo.feature @@ -0,0 +1,12 @@ +@SKIP +# Skipped awaiting a fix of the SSP, allowing for SFO authentications +Feature: A user authenticates with a service provider configured for second-factor-only + In order to login on a service provider + As a user + I must verify the second factor without authenticating with an identity provider + + Scenario: A user logs in using SFO + Given a service provider configured for second-factor-only + When I visit the service provider + And I verify the "yubikey" second factor + Then I am logged on the service provider diff --git a/stepup/tests/behat/features/src/Factory/CommandPayloadFactory.php b/stepup/tests/behat/features/src/Factory/CommandPayloadFactory.php new file mode 100644 index 0000000..d799ec6 --- /dev/null +++ b/stepup/tests/behat/features/src/Factory/CommandPayloadFactory.php @@ -0,0 +1,237 @@ +identityId, + $context->nameId, + $context->institution, + $context->commonName + ); + + case "Identity:ProveYubikeyPossession": + + /** @var SecondFactorToken $token */ + $token = $context->tokens[0]; + + $payload = '{ + "meta": { + "actor_id": "%s", + "actor_institution": "%s" + }, + "command": { + "name":"Identity:ProveYubikeyPossession", + "uuid":"%s", + "payload": { + "identity_id": "%s", + "second_factor_id": "%s", + "yubikey_public_id": "%s" + } + } + }'; + + return sprintf( + $payload, + $context->identityId, + $context->institution, + (string)Uuid::uuid4(), + $context->identityId, + $token->tokenId, + $token->identifier + ); + + case "Identity:ProvePhonePossession": + + /** @var SecondFactorToken $token */ + $token = $context->tokens[0]; + + $payload = '{ + "meta": { + "actor_id": "%s", + "actor_institution": "%s" + }, + "command": { + "name":"Identity:ProvePhonePossession", + "uuid":"%s", + "payload": { + "identity_id": "%s", + "second_factor_id": "%s", + "phone_number": "%s" + } + } + }'; + + return sprintf( + $payload, + $context->identityId, + $context->institution, + (string)Uuid::uuid4(), + $context->identityId, + $token->tokenId, + $token->identifier + ); + + case "Identity:ProveGssfPossession": + + /** @var SecondFactorToken $token */ + $token = $context->tokens[0]; + + $payload = '{ + "meta": { + "actor_id": "%s", + "actor_institution": "%s" + }, + "command": { + "name":"Identity:ProveGssfPossession", + "uuid":"%s", + "payload": { + "identity_id": "%s", + "second_factor_id": "%s", + "stepup_provider": "demo_gssp", + "gssf_id": "%s" + } + } + }'; + + return sprintf( + $payload, + $context->identityId, + $context->institution, + (string)Uuid::uuid4(), + $context->identityId, + $token->tokenId, + $token->identifier + ); + + case "Identity:VerifyEmail": + /** @var SecondFactorToken $token */ + $token = $context->tokens[0]; + + $payload = '{ + "meta": { + "actor_id": "%s", + "actor_institution": "%s" + }, + "command": { + "name":"Identity:VerifyEmail", + "uuid":"%s", + "payload": { + "identity_id": "%s", + "verification_nonce": "%s" + } + } + }'; + + return sprintf( + $payload, + $context->identityId, + $context->institution, + (string)Uuid::uuid4(), + $context->identityId, + $token->nonce + ); + + case "Identity:VetSecondFactor": + /** @var SecondFactorToken $token */ + $token = $context->tokens[0]; + + $payload = '{ + "meta": { + "actor_id": "%s", + "actor_institution": "%s" + }, + "command": { + "name":"Identity:VetSecondFactor", + "uuid":"%s", + "payload": { + "authority_id": "%s", + "identity_id": "%s", + "second_factor_id": "%s", + "registration_code": "%s", + "second_factor_type": "%s", + "second_factor_identifier": "%s", + "document_number": "123456", + "identity_verified": true + } + } + }'; + + return sprintf( + $payload, + $context->activationContext->actorId, + $context->institution, + (string)Uuid::uuid4(), + $context->activationContext->actorId, + $context->identityId, + $token->tokenId, + $context->activationContext->registrationCode, + $token->type, + $token->identifier + ); + } + } + + public function buildRolePayload($actorId, $identity, $institution, $role, $raInstitution) + { + $payload = '{ + "meta": { + "actor_id": "%s", + "actor_institution": "%s" + }, + "command": { + "name":"Identity:AccreditIdentity", + "uuid":"%s", + "payload": { + "identity_id": "%s", + "institution": "%s", + "role": "%s", + "location": "Location A", + "contact_information": "Contact INFO", + "ra_institution": "%s" + } + } + }'; + + return sprintf( + $payload, + $actorId, + $institution, + (string)Uuid::uuid4(), + $identity, + $institution, + $role, + $raInstitution + ); + } +} diff --git a/stepup/tests/behat/features/src/Repository/SecondFactorRepository.php b/stepup/tests/behat/features/src/Repository/SecondFactorRepository.php new file mode 100644 index 0000000..339feac --- /dev/null +++ b/stepup/tests/behat/features/src/Repository/SecondFactorRepository.php @@ -0,0 +1,52 @@ +connection = new PDO(sprintf($dsn, $dbName), $dbUser, $dbPassword); + } + + public function findNonceById($id) + { + $selectFormat = 'SELECT `verification_nonce` FROM `unverified_second_factor` WHERE `id` = :id;'; + $statement = $this->connection->prepare($selectFormat); + $statement->execute(['id' => $id]); + $configuration = $statement->fetch(); + return $configuration['verification_nonce']; + } + + public function getRegistrationCodeByIdentity($identityId) + { + $selectFormat = 'SELECT `registration_code` FROM `verified_second_factor` WHERE `identity_id` = :id ORDER BY `registration_requested_at` DESC LIMIT 1;'; + $statement = $this->connection->prepare($selectFormat); + $statement->execute(['id' => $identityId]); + $configuration = $statement->fetch(); + return $configuration['registration_code']; + } + + public function updateRegistrationCode($identityId, $registrationCode) + { + $sql = 'UPDATE `verified_second_factor` SET `registration_code` = :code WHERE `identity_id` = :id;'; + $statement = $this->connection->prepare($sql); + $statement->execute(['code' => $registrationCode, 'id' => $identityId]); + } +} diff --git a/stepup/tests/behat/features/src/ValueObject/ActivationContext.php b/stepup/tests/behat/features/src/ValueObject/ActivationContext.php new file mode 100644 index 0000000..3d71b68 --- /dev/null +++ b/stepup/tests/behat/features/src/ValueObject/ActivationContext.php @@ -0,0 +1,9 @@ +identityId = $identityId; + $identity->nameId = $nameId; + $identity->commonName = $commonName; + $identity->institution = $institution; + $identity->tokens = $tokens; + + return $identity; + } +} \ No newline at end of file diff --git a/stepup/tests/behat/features/src/ValueObject/InstitutionConfiguration.php b/stepup/tests/behat/features/src/ValueObject/InstitutionConfiguration.php new file mode 100644 index 0000000..34f69a6 --- /dev/null +++ b/stepup/tests/behat/features/src/ValueObject/InstitutionConfiguration.php @@ -0,0 +1,102 @@ +getRoles())) { + throw new Exception('Invalid role requested'); + } + + if (!isset($this->configuration[$institution])) { + $this->configuration[$institution] = []; + } + + if (!isset($this->configuration[$institution][$role])) { + $this->configuration[$institution][$role] = []; + } + + if (!in_array($raInstitution, $this->configuration[$institution][$role])) { + $this->configuration[$institution][$role][] = $raInstitution; + } + } + + + /** + * @return string + */ + public function getPayload() + { + $payload = []; + foreach ($this->configuration as $institutionName => $institutionConfiguration) { + + // build permission payload + foreach ($this->getRoles() as $role) { + if (!isset($institutionConfiguration[$role])) { + $institutionConfiguration[$role] = []; + } + } + $permissions = $this->buildPermissionPayload($institutionConfiguration); + + // build institution payload + $payload[$institutionName] = sprintf(' + "%s": { + "use_ra_locations": true, + "show_raa_contact_information": true, + "verify_email": true, + "self_vet": false, + "number_of_tokens_per_identity": 2, + "allowed_second_factors": [], + %s + }', $institutionName, $permissions); + } + + $result = '{'.implode(',', $payload)."\n}"; + + return $result; + } + + /** + * @param $institutions + * @return bool|string + */ + private function buildPermissionPayload($institutions) + { + return substr(json_encode($institutions), 1, -1); + } +} diff --git a/stepup/tests/behat/features/src/ValueObject/SecondFactorToken.php b/stepup/tests/behat/features/src/ValueObject/SecondFactorToken.php new file mode 100644 index 0000000..2964d6a --- /dev/null +++ b/stepup/tests/behat/features/src/ValueObject/SecondFactorToken.php @@ -0,0 +1,27 @@ +tokenId = $tokenId; + $token->type = $type; + $token->identifier = $identifier; + + return $token; + } +} \ No newline at end of file diff --git a/stepup/tests/behat/features/sso.feature b/stepup/tests/behat/features/sso.feature new file mode 100644 index 0000000..8b2b414 --- /dev/null +++ b/stepup/tests/behat/features/sso.feature @@ -0,0 +1,26 @@ +Feature: A user signs in on a service provider + In order to login on a service provider + As a user + I must verify the second factor after authenticating with an identity provider + + Scenario: A user logs in using single-signon + Given a service provider configured for single-signon + When I visit the service provider + And I authenticate as "admin" with the identity provider + And I verify the "yubikey" second factor + Then I am logged on the service provider + + Scenario: A user cancels the second factor authentication + Given a service provider configured for single-signon + When I visit the service provider + And I authenticate as "admin" with the identity provider + And I cancel the "yubikey" second factor authentication + Then I see an error at the service provider + + Scenario: A user logs in using single-signon without second factor requirement + Given a service provider configured for single-signon + And the service provider requires no second factor + When I visit the service provider + And I authenticate with the identity provider + Then second factor authentication is not initiated + And I am logged on the service provider diff --git a/stepup/tests/behat/fixtures/events.sql b/stepup/tests/behat/fixtures/events.sql new file mode 100644 index 0000000..5629575 --- /dev/null +++ b/stepup/tests/behat/fixtures/events.sql @@ -0,0 +1,84 @@ +-- MySQL dump 10.13 Distrib 5.7.23, for Linux (x86_64) +-- +-- Host: 127.0.0.1 Database: middleware_test +-- ------------------------------------------------------ +-- Server version 5.5.5-10.0.35-MariaDB-wsrep + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `event_stream` +-- + +DROP TABLE IF EXISTS `event_stream`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `event_stream` ( + `uuid` varchar(36) COLLATE utf8_unicode_ci NOT NULL, + `playhead` int(11) NOT NULL, + `metadata` text COLLATE utf8_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8_unicode_ci NOT NULL, + `recorded_on` varchar(32) COLLATE utf8_unicode_ci NOT NULL, + `type` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + PRIMARY KEY (`uuid`,`playhead`), + KEY `type` (`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `event_stream` +-- + +LOCK TABLES `event_stream` WRITE; +/*!40000 ALTER TABLE `event_stream` DISABLE KEYS */; +INSERT INTO `event_stream` VALUES ('0007699b-7a29-4526-9e08-fe291806361f',0,'{\"class\":\"Broadway\\\\Domain\\\\Metadata\",\"payload\":[]}','{\"class\":\"Surfnet\\\\Stepup\\\\Identity\\\\Event\\\\IdentityCreatedEvent\",\"payload\":{\"id\":\"0007699b-7a29-4526-9e08-fe291806361f\",\"institution\":\"dev.openconext.local\",\"name_id\":\"urn:collab:person:dev.openconext.local:sraa2\",\"preferred_locale\":\"en_GB\"}}','2023-09-14T12:22:22.541496+00:00','Surfnet.Stepup.Identity.Event.IdentityCreatedEvent'),('0007699b-7a29-4526-9e08-fe291806361f',1,'{\"class\":\"Broadway\\\\Domain\\\\Metadata\",\"payload\":[]}','{\"class\":\"Surfnet\\\\Stepup\\\\Identity\\\\Event\\\\YubikeySecondFactorBootstrappedEvent\",\"payload\":{\"identity_id\":\"0007699b-7a29-4526-9e08-fe291806361f\",\"name_id\":\"urn:collab:person:dev.openconext.local:sraa2\",\"identity_institution\":\"dev.openconext.local\",\"preferred_locale\":\"en_GB\",\"second_factor_id\":\"f2b1e616-ecde-458b-9f12-1536ad63ded0\"}}','2023-09-14T12:22:22.545350+00:00','Surfnet.Stepup.Identity.Event.YubikeySecondFactorBootstrappedEvent'),('12345678-abcd-4321-abcd-123456789012',0,'{\"class\":\"Broadway\\\\Domain\\\\Metadata\",\"payload\":[]}','{\"class\":\"Surfnet\\\\Stepup\\\\Configuration\\\\Event\\\\NewConfigurationCreatedEvent\",\"payload\":{\"id\":\"12345678-abcd-4321-abcd-123456789012\"}}','2023-07-19T06:46:34.940735+00:00','Surfnet.Stepup.Configuration.Event.NewConfigurationCreatedEvent'),('12345678-abcd-4321-abcd-123456789012',1,'{\"class\":\"Broadway\\\\Domain\\\\Metadata\",\"payload\":[]}','{\"class\":\"Surfnet\\\\Stepup\\\\Configuration\\\\Event\\\\ConfigurationUpdatedEvent\",\"payload\":{\"id\":\"12345678-abcd-4321-abcd-123456789012\",\"new_configuration\":{\"sraa\":[\"urn:collab:person:dev.openconext.local:admin\",\"urn:collab:person:dev.openconext.local:pieter\",\"urn:collab:person:dev.openconext.local:joost\"],\"email_templates\":{\"confirm_email\":{\"en_GB\":\"

Dear {{ commonName }},<\\/p>

Thank you for registering your token. Please visit this link to verify your email address:<\\/p>

{{ verificationUrl }}<\\/a><\\/p>

If you can not click on the URL, please copy the link and paste it in the address bar of your browser.<\\/p>\",\"nl_NL\":\"

Beste {{ commonName }},<\\/p>

Bedankt voor het registreren van je token. Klik op onderstaande link om je e-mailadres te bevestigen:<\\/p>

{{ verificationUrl }}<\\/a><\\/p>

Is klikken op de link niet mogelijk? Kopieer dan de link en plak deze in de adresbalk van je browser.<\\/p>\"},\"registration_code_with_ras\":{\"en_GB\":\"

Dear {{ commonName }},<\\/p>

Thank you for registering your token. Please visit one of the locations below within 14 days to get your token activated. After {{ expirationDate | localizeddate(\'full\', \'none\', locale) }} your activation code is no longer valid.<\\/p>

Please bring the following:<\\/p>