diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 6dfe40d6..50858d85 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -10,61 +10,65 @@ additional_hostnames: [] additional_fqdns: [] database: type: mariadb - version: "10.3" -nfs_mount_enabled: false -mutagen_enabled: false + version: "10.6" host_db_port: "59002" use_dns_when_possible: true composer_version: "2" web_environment: [] nodejs_version: "16" +corepack_enable: false -# Key features of ddev's config.yaml: +# Key features of DDEV's config.yaml: # name: # Name of the project, automatically provides # http://projectname.ddev.site and https://projectname.ddev.site -# type: # drupal6/7/8, backdrop, typo3, wordpress, php +# type: # backdrop, cakephp, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress +# See https://ddev.readthedocs.io/en/stable/users/quickstart/ for more +# information on the different project types # docroot: # Relative path to the directory containing index.php. -# php_version: "7.4" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2" +# php_version: "8.3" # PHP version to use, "5.6" through "8.4" # You can explicitly specify the webimage but this -# is not recommended, as the images are often closely tied to ddev's' behavior, +# is not recommended, as the images are often closely tied to DDEV's' behavior, # so this can break upgrades. # webimage: # nginx/php docker image. # database: -# type: # mysql, mariadb -# version: # database version, like "10.3" or "8.0" -# Note that mariadb_version or mysql_version from v1.18 and earlier -# will automatically be converted to this notation with just a "ddev config --auto" +# type: # mysql, mariadb, postgres +# version: # database version, like "10.11" or "8.0" +# MariaDB versions can be 5.5-10.8, 10.11, and 11.4. +# MySQL versions can be 5.5-8.0. +# PostgreSQL versions can be 9-17. -# router_http_port: # Port to be used for http (defaults to port 80) -# router_https_port: # Port for https (defaults to 443) +# router_http_port: # Port to be used for http (defaults to global configuration, usually 80) +# router_https_port: # Port for https (defaults to global configuration, usually 443) -# xdebug_enabled: false # Set to true to enable xdebug and "ddev start" or "ddev restart" +# xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart" # Note that for most people the commands -# "ddev xdebug" to enable xdebug and "ddev xdebug off" to disable it work better, -# as leaving xdebug enabled all the time is a big performance hit. +# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, +# as leaving Xdebug enabled all the time is a big performance hit. -# xhprof_enabled: false # Set to true to enable xhprof and "ddev start" or "ddev restart" +# xhprof_enabled: false # Set to true to enable Xhprof and "ddev start" or "ddev restart" # Note that for most people the commands -# "ddev xhprof" to enable xhprof and "ddev xhprof off" to disable it work better, -# as leaving xhprof enabled all the time is a big performance hit. +# "ddev xhprof" to enable Xhprof and "ddev xhprof off" to disable it work better, +# as leaving Xhprof enabled all the time is a big performance hit. -# webserver_type: nginx-fpm # or apache-fpm +# webserver_type: nginx-fpm or apache-fpm # timezone: Europe/Berlin +# If timezone is unset, DDEV will attempt to derive it from the host system timezone +# using the $TZ environment variable or the /etc/localtime symlink. # This is the timezone used in the containers and by PHP; # it can be set to any valid timezone, # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones # For example Europe/Dublin or MST7MDT # composer_root: -# Relative path to the composer root directory from the project root. This is +# Relative path to the Composer root directory from the project root. This is # the directory which contains the composer.json and where all Composer related # commands are executed. @@ -77,12 +81,17 @@ nodejs_version: "16" # - preview # - snapshot # Alternatively, an explicit Composer version may be specified, for example "2.2.18". -# To reinstall Composer after the image was built, run "ddev debug refresh". +# To reinstall Composer after the image was built, run "ddev debug rebuild". -# nodejs_version: "16" -# change from the default system Node.js version to another supported version, like 12, 14, 17, 18. -# Note that you can use 'ddev nvm' or nvm inside the web container to provide nearly any -# Node.js version, including v6, etc. +# nodejs_version: "22" +# change from the default system Node.js version to any other version. +# See https://ddev.readthedocs.io/en/stable/users/configuration/config/#nodejs_version for more information +# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation, +# Note that using of 'ddev nvm' is discouraged because "nodejs_version" is much easier to use, +# can specify any version, and is more robust than using 'nvm'. + +# corepack_enable: false +# Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm # additional_hostnames: # - somename @@ -96,10 +105,26 @@ nodejs_version: "16" # would provide http and https URLs for "example.com" and "sub1.example.com" # Please take care with this because it can cause great confusion. -# upload_dir: custom/upload/dir -# would set the destination path for ddev import-files to /custom/upload/dir -# When mutagen is enabled this path is bind-mounted so that all the files -# in the upload_dir don't have to be synced into mutagen +# upload_dirs: "custom/upload/dir" +# +# upload_dirs: +# - custom/upload/dir +# - ../private +# +# would set the destination paths for ddev import-files to /custom/upload/dir +# When Mutagen is enabled this path is bind-mounted so that all the files +# in the upload_dirs don't have to be synced into Mutagen. + +# disable_upload_dirs_warning: false +# If true, turns off the normal warning that says +# "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set" + +# ddev_version_constraint: "" +# Example: +# ddev_version_constraint: ">= 1.22.4" +# This will enforce that the running ddev version is within this constraint. +# See https://github.com/Masterminds/semver#checking-version-constraints for +# supported constraint formats # working_dir: # web: /var/www/html @@ -108,20 +133,25 @@ nodejs_version: "16" # These values specify the destination directory for ddev ssh and the # directory in which commands passed into ddev exec are run. -# omit_containers: [db, dba, ddev-ssh-agent] +# omit_containers: [db, ddev-ssh-agent] # Currently only these containers are supported. Some containers can also be # omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit -# the "db" container, several standard features of ddev that access the +# the "db" container, several standard features of DDEV that access the # database container will be unusable. In the global configuration it is also # possible to omit ddev-router, but not here. -# nfs_mount_enabled: false -# Great performance improvement but requires host configuration first. -# See https://ddev.readthedocs.io/en/latest/users/install/performance/#nfs - -# mutagen_enabled: false -# Performance improvement using mutagen asynchronous updates. -# See https://ddev.readthedocs.io/en/latest/users/install/performance/#mutagen +# performance_mode: "global" +# DDEV offers performance optimization strategies to improve the filesystem +# performance depending on your host system. Should be configured globally. +# +# If set, will override the global config. Possible values are: +# - "global": uses the value from the global config. +# - "none": disables performance optimization for this project. +# - "mutagen": enables Mutagen for this project. +# - "nfs": enables NFS for this project. +# +# See https://ddev.readthedocs.io/en/stable/users/install/performance/#nfs +# See https://ddev.readthedocs.io/en/stable/users/install/performance/#mutagen # fail_on_hook_fail: False # Decide whether 'ddev start' should be interrupted by a failing hook @@ -142,20 +172,12 @@ nodejs_version: "16" # The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic # unless explicitly specified. -# phpmyadmin_port: "8036" -# phpmyadmin_https_port: "8037" -# The PHPMyAdmin ports can be changed from the default 8036 and 8037 - -# host_phpmyadmin_port: "8036" -# The phpmyadmin (dba) port is not normally bound on the host at all, instead being routed -# through ddev-router, but it can be specified and bound. - -# mailhog_port: "8025" -# mailhog_https_port: "8026" -# The MailHog ports can be changed from the default 8025 and 8026 +# mailpit_http_port: "8025" +# mailpit_https_port: "8026" +# The Mailpit ports can be changed from the default 8025 and 8026 -# host_mailhog_port: "8025" -# The mailhog port is not normally bound on the host at all, instead being routed +# host_mailpit_port: "8025" +# The mailpit port is not normally bound on the host at all, instead being routed # through ddev-router, but it can be bound directly to localhost if specified here. # webimage_extra_packages: [php7.4-tidy, php-bcmath] @@ -178,32 +200,32 @@ nodejs_version: "16" # ngrok_args: --basic-auth username:pass1234 # Provide extra flags to the "ngrok http" command, see -# https://ngrok.com/docs#http or run "ngrok http -h" +# https://ngrok.com/docs/ngrok-agent/config or run "ngrok http -h" # disable_settings_management: false -# If true, ddev will not create CMS-specific settings files like -# Drupal's settings.php/settings.ddev.php or TYPO3's AdditionalConfiguration.php +# If true, DDEV will not create CMS-specific settings files like +# Drupal's settings.php/settings.ddev.php or TYPO3's additional.php # In this case the user must provide all such settings. # You can inject environment variables into the web container with: # web_environment: -# - SOMEENV=somevalue -# - SOMEOTHERENV=someothervalue +# - SOMEENV=somevalue +# - SOMEOTHERENV=someothervalue # no_project_mount: false -# (Experimental) If true, ddev will not mount the project into the web container; +# (Experimental) If true, DDEV will not mount the project into the web container; # the user is responsible for mounting it manually or via a script. # This is to enable experimentation with alternate file mounting strategies. # For advanced users only! # bind_all_interfaces: false # If true, host ports will be bound on all network interfaces, -# not just the localhost interface. This means that ports +# not the localhost interface only. This means that ports # will be available on the local network if the host firewall # allows it. # default_container_timeout: 120 -# The default time that ddev waits for all containers to become ready can be increased from +# The default time that DDEV waits for all containers to become ready can be increased from # the default 120. This helps in importing huge databases, for example. #web_extra_exposed_ports: @@ -216,12 +238,16 @@ nodejs_version: "16" # https_port: 4000 # http_port: 3999 # Allows a set of extra ports to be exposed via ddev-router +# Fill in all three fields even if you don’t intend to use the https_port! +# If you don’t add https_port, then it defaults to 0 and ddev-router will fail to start. +# # The port behavior on the ddev-webserver must be arranged separately, for example # using web_extra_daemons. # For example, with a web app on port 3000 inside the container, this config would # expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998 # web_extra_exposed_ports: -# - container_port: 3000 +# - name: myapp +# container_port: 3000 # http_port: 9998 # https_port: 9999 @@ -236,10 +262,10 @@ nodejs_version: "16" # override_config: false # By default, config.*.yaml files are *merged* into the configuration # But this means that some things can't be overridden -# For example, if you have 'nfs_mount_enabled: true'' you can't override it with a merge +# For example, if you have 'use_dns_when_possible: true'' you can't override it with a merge # and you can't erase existing hooks or all environment variables. # However, with "override_config: true" in a particular config.*.yaml file, -# 'nfs_mount_enabled: false' can override the existing values, and +# 'use_dns_when_possible: false' can override the existing values, and # hooks: # post-start: [] # or @@ -249,13 +275,14 @@ nodejs_version: "16" # can have their intended affect. 'override_config' affects only behavior of the # config.*.yaml file it exists in. -# Many ddev commands can be extended to run tasks before or after the -# ddev command is executed, for example "post-start", "post-import-db", +# Many DDEV commands can be extended to run tasks before or after the +# DDEV command is executed, for example "post-start", "post-import-db", # "pre-composer", "post-composer" # See https://ddev.readthedocs.io/en/stable/users/extend/custom-commands/ for more # information on the commands that can be extended and the tasks you can define # for them. Example: #hooks: # post-import-db: -# - exec: drush cr -# - exec: drush updb +# - exec: drush sql:sanitize +# - exec: drush updatedb +# - exec: drush cache:rebuild diff --git a/.ddev/php/xdebug_client_port.ini b/.ddev/php/xdebug_client_port.ini index 7bead899..8fc3094d 100644 --- a/.ddev/php/xdebug_client_port.ini +++ b/.ddev/php/xdebug_client_port.ini @@ -1,2 +1,3 @@ [PHP] xdebug.client_port=9003 +xdebug.mode=debug diff --git a/.github/workflows/phpanalysis.yml b/.github/workflows/phpanalysis.yml index 7465e7c1..0f2a299e 100644 --- a/.github/workflows/phpanalysis.yml +++ b/.github/workflows/phpanalysis.yml @@ -77,4 +77,4 @@ jobs: - name: Run PHPUnit tests run: | - vendor/bin/phpunit + vendor/bin/phpunit --testsuite unit,kernel diff --git a/README.md b/README.md index e5ad84b4..6c222767 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,14 @@ You may use the DDEV Integration plugin or configure the database connection as - User: `db` - Password: `db` +### PHPUnit Tests + +We have four different testsuites: `unit`, `kernel`, `functional` and `existing-site`. You can run them locally with the following command. + +``` +vendor/bin/phpunit --testsuite unit --configuration phpunit.ddev.xml +``` + ### PHPStorm CodeSniffer configuration * https://www.drupal.org/node/1419988 diff --git a/composer-manifest.yaml b/composer-manifest.yaml index 1cac5f6a..44fc3e09 100644 --- a/composer-manifest.yaml +++ b/composer-manifest.yaml @@ -52,8 +52,8 @@ packages: drupal/entity: 1.5.0 drupal/file_mdm: 3.1.0 drupal/filefield_paths: 1.0.0-beta8 - drupal/gin: 3.0.0-rc16 - drupal/gin_toolbar: 1.0.0-rc8 + drupal/gin: 4.0.0 + drupal/gin_toolbar: 1.0.0 drupal/image_style_warmer: 1.3.0-rc1 drupal/imagemagick: 4.0.2 drupal/jsonapi_boost: 2.0.4 @@ -91,13 +91,13 @@ packages: guzzlehttp/promises: 2.0.4 guzzlehttp/psr7: 2.7.0 jangregor/phpstan-prophecy: 1.0.2 - joachim-n/composer-manifest: 1.1.6 + joachim-n/composer-manifest: 1.1.7 justinrainbow/json-schema: 5.3.0 lcobucci/clock: 3.0.0 lcobucci/jwt: 4.3.0 league/container: 4.2.4 league/event: 2.2.0 - league/oauth2-server: 8.5.4 + league/oauth2-server: 8.5.5 league/uri: 7.5.1 league/uri-interfaces: 7.5.0 lullabot/mink-selenium2-driver: v1.7.4 @@ -160,7 +160,7 @@ packages: ramsey/collection: 2.0.0 ramsey/uuid: 4.7.6 react/promise: v3.2.0 - roave/security-advisories: 'dev-latest:4aa68d3ea343b9576b5fdebef1332f701a7bc994' + roave/security-advisories: 'dev-latest:abbccc97f36a9c78f033525c019d310433f22b57' sebastian/cli-parser: 1.0.2 sebastian/code-unit: 1.0.8 sebastian/code-unit-reverse-lookup: 2.0.3 @@ -229,3 +229,4 @@ packages: vipnytt/sitemapparser: v1.3.0 webflo/drupal-finder: 1.3.1 webmozart/assert: 1.11.0 + weitzman/drupal-test-traits: 2.5.0 diff --git a/composer.json b/composer.json index 930b5860..d6496606 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "drupal/core-vendor-hardening": "^10.4", "drupal/entity": "^1.4", "drupal/filefield_paths": "^1.0@beta", - "drupal/gin": "^3.0@RC", + "drupal/gin": "^4.0", "drupal/image_style_warmer": "^1.3", "drupal/imagemagick": "^4.0", "drupal/jsonapi_boost": "^2.0", @@ -68,6 +68,9 @@ }, "extra": { "drupal-scaffold": { + "allowed-packages": { + "weitzman/drupal-test-traits": true + }, "locations": { "web-root": "web/" } @@ -149,7 +152,8 @@ "phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^1.10", "phpstan/phpstan-deprecation-rules": "^1.1", - "roave/security-advisories": "dev-latest" + "roave/security-advisories": "dev-latest", + "weitzman/drupal-test-traits": "^2.5" }, "scripts": { "post-drupal-scaffold-cmd": [ diff --git a/composer.lock b/composer.lock index cbc7a76f..db1d5356 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6819bcf289f29ff29eb2676d211694d4", + "content-hash": "cf3b78f3be287d9b5f4bde61fa429daa", "packages": [ { "name": "asm89/stack-cors", @@ -2086,30 +2086,30 @@ }, { "name": "drupal/gin", - "version": "3.0.0-rc16", + "version": "4.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/gin.git", - "reference": "8.x-3.0-rc16" + "reference": "4.0.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/gin-8.x-3.0-rc16.zip", - "reference": "8.x-3.0-rc16", - "shasum": "29fecc9fbcf40b77ffde6b2e5e4400ba938ab3ee" + "url": "https://ftp.drupal.org/files/projects/gin-4.0.0.zip", + "reference": "4.0.0", + "shasum": "8607d9f225b550c9c2f0a84d0371adabc0ac42c1" }, "require": { - "drupal/core": "^9 || ^10 || ^11", + "drupal/core": "^10 || ^11", "drupal/gin_toolbar": "^1.0@beta" }, "type": "drupal-theme", "extra": { "drupal": { - "version": "8.x-3.0-rc16", - "datestamp": "1734527306", + "version": "4.0.0", + "datestamp": "1734696940", "security-coverage": { - "status": "not-covered", - "message": "RC releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -2152,17 +2152,17 @@ }, { "name": "drupal/gin_toolbar", - "version": "1.0.0-rc8", + "version": "1.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/gin_toolbar.git", - "reference": "8.x-1.0-rc8" + "reference": "8.x-1.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/gin_toolbar-8.x-1.0-rc8.zip", - "reference": "8.x-1.0-rc8", - "shasum": "bc4c199708e0f47a2893cc93d2e1cd7d9e5d21c3" + "url": "https://ftp.drupal.org/files/projects/gin_toolbar-8.x-1.0.zip", + "reference": "8.x-1.0", + "shasum": "907a3c2202824168da98c8b1cede1af117f7adf2" }, "require": { "drupal/core": "^9 || ^10 || ^11" @@ -2170,11 +2170,11 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.0-rc8", - "datestamp": "1734527411", + "version": "8.x-1.0", + "datestamp": "1734698810", "security-coverage": { - "status": "not-covered", - "message": "RC releases are not covered by Drupal security advisories." + "status": "covered", + "message": "Covered by Drupal's security advisory policy" } } }, @@ -3948,16 +3948,16 @@ }, { "name": "joachim-n/composer-manifest", - "version": "1.1.6", + "version": "1.1.7", "source": { "type": "git", "url": "https://github.com/joachim-n/composer-manifest.git", - "reference": "4c62e3f1fd80d350447c7c9365a826420b7c80b2" + "reference": "b5ac65194179ccecbdb14e02f4cbfa82cc28fba7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joachim-n/composer-manifest/zipball/4c62e3f1fd80d350447c7c9365a826420b7c80b2", - "reference": "4c62e3f1fd80d350447c7c9365a826420b7c80b2", + "url": "https://api.github.com/repos/joachim-n/composer-manifest/zipball/b5ac65194179ccecbdb14e02f4cbfa82cc28fba7", + "reference": "b5ac65194179ccecbdb14e02f4cbfa82cc28fba7", "shasum": "" }, "require": { @@ -3986,9 +3986,9 @@ ], "support": { "issues": "https://github.com/joachim-n/composer-manifest/issues", - "source": "https://github.com/joachim-n/composer-manifest/tree/1.1.6" + "source": "https://github.com/joachim-n/composer-manifest/tree/1.1.7" }, - "time": "2024-07-22T08:47:29+00:00" + "time": "2024-12-21T10:53:36+00:00" }, { "name": "justinrainbow/json-schema", @@ -4331,16 +4331,16 @@ }, { "name": "league/oauth2-server", - "version": "8.5.4", + "version": "8.5.5", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-server.git", - "reference": "ab7714d073844497fd222d5d0a217629089936bc" + "reference": "cc8778350f905667e796b3c2364a9d3bd7a73518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/ab7714d073844497fd222d5d0a217629089936bc", - "reference": "ab7714d073844497fd222d5d0a217629089936bc", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/cc8778350f905667e796b3c2364a9d3bd7a73518", + "reference": "cc8778350f905667e796b3c2364a9d3bd7a73518", "shasum": "" }, "require": { @@ -4407,7 +4407,7 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-server/issues", - "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.4" + "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.5" }, "funding": [ { @@ -4415,7 +4415,7 @@ "type": "github" } ], - "time": "2023-08-25T22:35:12+00:00" + "time": "2024-12-20T23:06:10+00:00" }, { "name": "league/uri", @@ -9375,13 +9375,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { @@ -13324,12 +13324,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "4aa68d3ea343b9576b5fdebef1332f701a7bc994" + "reference": "abbccc97f36a9c78f033525c019d310433f22b57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/4aa68d3ea343b9576b5fdebef1332f701a7bc994", - "reference": "4aa68d3ea343b9576b5fdebef1332f701a7bc994", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/abbccc97f36a9c78f033525c019d310433f22b57", + "reference": "abbccc97f36a9c78f033525c019d310433f22b57", "shasum": "" }, "conflict": { @@ -13618,6 +13618,7 @@ "james-heinrich/phpthumb": "<1.7.12", "jasig/phpcas": "<1.3.3", "jcbrand/converse.js": "<3.3.3", + "joelbutcher/socialstream": "<6.2", "johnbillion/wp-crontrol": "<1.16.2", "joomla/application": "<1.0.13", "joomla/archive": "<1.1.12|>=2,<2.0.1", @@ -13916,7 +13917,7 @@ "snipe/snipe-it": "<=7.0.13", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", - "spatie/browsershot": "<5.0.2", + "spatie/browsershot": "<5.0.3", "spatie/image-optimizer": "<1.7.3", "spencer14420/sp-php-email-handler": "<1", "spipu/html2pdf": "<5.2.8", @@ -14168,7 +14169,7 @@ "type": "tidelift" } ], - "time": "2024-12-18T20:05:40+00:00" + "time": "2024-12-20T16:05:39+00:00" }, { "name": "sebastian/cli-parser", @@ -16195,13 +16196,77 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "weitzman/drupal-test-traits", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/dtt.git", + "reference": "664201b00140f9b935fcb9f3eef012c6638ec0c5" + }, + "dist": { + "type": "zip", + "url": "https://git.drupalcode.org/api/v4/projects/project%2Fdtt/repository/archive.zip?sha=664201b00140f9b935fcb9f3eef012c6638ec0c5", + "reference": "664201b00140f9b935fcb9f3eef012c6638ec0c5", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "drupal/core": "<=10.2" + }, + "require-dev": { + "drupal/mailsystem": "^4", + "drush/drush": "^12.5 || ^13", + "phpspec/prophecy-phpunit": "^2", + "webflo/drupal-finder": "^1.3.1" + }, + "type": "library", + "extra": { + "drupal-scaffold": { + "locations": { + "web-root": "web/" + }, + "file-mapping": { + "[project-root]/.gitignore": false, + "[project-root]/.editorconfig": false, + "[project-root]/.gitattributes": false + } + }, + "installer-paths": { + "web/core": [ + "type:drupal-core" + ] + } + }, + "autoload": { + "psr-4": { + "weitzman\\DrupalTestTraits\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Moshe Weitzman", + "email": "weitzman@tejasa.com" + } + ], + "description": "Traits for testing Drupal sites that have user content (versus unpopulated sites).", + "support": { + "source": "https://git.drupalcode.org/project/dtt/-/tree/2.5.0" + }, + "time": "2024-10-30T23:28:39+00:00" } ], "aliases": [], "minimum-stability": "beta", "stability-flags": { "drupal/filefield_paths": 10, - "drupal/gin": 5, "drupal/multivalue_form_element": 10, "drupal/openapi_ui": 5, "drupal/openapi_ui_redoc": 5, @@ -16209,10 +16274,10 @@ }, "prefer-stable": true, "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, + "platform": [], + "platform-dev": [], "platform-overrides": { "php": "8.1" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } diff --git a/phpunit.ddev.xml b/phpunit.ddev.xml index 2533a59f..b53d3e42 100644 --- a/phpunit.ddev.xml +++ b/phpunit.ddev.xml @@ -1,42 +1,61 @@ + + cacheResult="false"> + + + - + + + - - web/modules/custom + + ./modules/custom/*/tests/src/Unit + ./modules/custom/*/*/tests/src/Unit + + + ./modules/custom/*/tests/src/ExistingSite + ./modules/custom/*/*/tests/src/ExistingSite + + + ./modules/custom/*/tests/src/Kernel + ./modules/custom/*/*/tests/src/Kernel + + + ./modules/custom/*/tests/src/Functional + ./modules/custom/*/*/tests/src/Functional + + - web/modules/custom + ./web/modules/custom - web/modules/custom/*/src/Tests - web/modules/custom/*/tests - web/modules/custom/*/*/src/Tests - web/modules/custom/*/*/tests + ./web/modules/custom/*/tests + ./web/modules/custom/*/*/tests + diff --git a/web/.gitignore b/web/.gitignore index d658a1a5..643c5bf7 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -25,6 +25,7 @@ example.gitignore index.php INSTALL.txt LICENSE.txt +README.md README.txt robots.txt update.php diff --git a/web/README.md b/web/README.md deleted file mode 100644 index c3e53713..00000000 --- a/web/README.md +++ /dev/null @@ -1,75 +0,0 @@ -Drupal Logo - -Drupal is an open source content management platform supporting a variety of -websites ranging from personal weblogs to large community-driven websites. For -more information, visit the Drupal website, [Drupal.org][Drupal.org], and join -the [Drupal community][Drupal community]. - -## Contributing - -Drupal is developed on [Drupal.org][Drupal.org], the home of the international -Drupal community since 2001! - -[Drupal.org][Drupal.org] hosts Drupal's [GitLab repository][GitLab repository], -its [issue queue][issue queue], and its [documentation][documentation]. Before -you start working on code, be sure to search the [issue queue][issue queue] and -create an issue if your aren't able to find an existing issue. - -Every issue on Drupal.org automatically creates a new community-accessible fork -that you can contribute to. Learn more about the code contribution process on -the [Issue forks & merge requests page][issue forks]. - -## Usage - -For a brief introduction, see [USAGE.txt](/core/USAGE.txt). You can also find -guides, API references, and more by visiting Drupal's [documentation -page][documentation]. - -You can quickly extend Drupal's core feature set by installing any of its -[thousands of free and open source modules][modules]. With Drupal and its -module ecosystem, you can often build most or all of what your project needs -before writing a single line of code. - -## Changelog - -Drupal keeps detailed [change records][changelog]. You can search Drupal's -changes for a record of every notable breaking change and new feature since -2011. - -## Security - -For a list of security announcements, see the [Security advisories -page][Security advisories] (available as [an RSS feed][security RSS]). This -page also describes how to subscribe to these announcements via email. - -For information about the Drupal security process, or to find out how to report -a potential security issue to the Drupal security team, see the [Security team -page][security team]. - -## Need a helping hand? - -Visit the [Support page][support] or browse [over a thousand Drupal -providers][service providers] offering design, strategy, development, and -hosting services. - -## Legal matters - -Know your rights when using Drupal by reading Drupal core's -[license](/core/LICENSE.txt). - -Learn about the [Drupal trademark and logo policy here][trademark]. - -[Drupal.org]: https://www.drupal.org -[Drupal community]: https://www.drupal.org/community -[GitLab repository]: https://git.drupalcode.org/project/drupal -[issue queue]: https://www.drupal.org/project/issues/drupal -[issue forks]: https://www.drupal.org/drupalorg/docs/gitlab-integration/issue-forks-merge-requests -[documentation]: https://www.drupal.org/documentation -[changelog]: https://www.drupal.org/list-changes/drupal -[modules]: https://www.drupal.org/project/project_module -[security advisories]: https://www.drupal.org/security -[security RSS]: https://www.drupal.org/security/rss.xml -[security team]: https://www.drupal.org/drupal-security-team -[service providers]: https://www.drupal.org/drupal-services -[support]: https://www.drupal.org/support -[trademark]: https://www.drupal.com/trademark diff --git a/web/modules/custom/interfaces/consumer_permissions/src/ConsumerPermissionsAuthDecorator.php b/web/modules/custom/interfaces/consumer_permissions/src/ConsumerPermissionsAuthDecorator.php index edc9d341..eb0ca63d 100644 --- a/web/modules/custom/interfaces/consumer_permissions/src/ConsumerPermissionsAuthDecorator.php +++ b/web/modules/custom/interfaces/consumer_permissions/src/ConsumerPermissionsAuthDecorator.php @@ -95,8 +95,9 @@ public function lookupAccount($identifier): UserInterface|false { * {@inheritdoc} */ public function authenticateAccount(UserInterface $account, string $password): bool { + // Note that we are using the mail_login module. // @todo Clean up implementation when removing the deprecated interface. - return $this->authenticate($account->getAccountName(), $password); + return $this->authenticate($account->getEmail(), $password); } } diff --git a/web/modules/custom/interfaces/jsonapi_obscurity/tests/src/Kernel/JsonApiObscurityTest.php b/web/modules/custom/interfaces/jsonapi_obscurity/tests/src/Kernel/JsonApiObscurityTest.php index 74f3f51a..349568ab 100644 --- a/web/modules/custom/interfaces/jsonapi_obscurity/tests/src/Kernel/JsonApiObscurityTest.php +++ b/web/modules/custom/interfaces/jsonapi_obscurity/tests/src/Kernel/JsonApiObscurityTest.php @@ -26,22 +26,16 @@ class JsonApiObscurityTest extends KernelTestBase { /** * The JSON:API obscurity prefix. - * - * @var string */ protected string $obscurityPrefix; /** * The JSON:API base path. - * - * @var string */ protected string $jsonApiBasePath; /** * The test node. - * - * @var \Drupal\node\NodeInterface */ protected NodeInterface $node; diff --git a/web/modules/custom/projects/projects/projects.info.yml b/web/modules/custom/projects/projects/projects.info.yml index 5a0f3416..6c4da9d8 100644 --- a/web/modules/custom/projects/projects/projects.info.yml +++ b/web/modules/custom/projects/projects/projects.info.yml @@ -3,14 +3,15 @@ description: 'Defines Project entities and handles business logic.' package: Projects type: module -core_version_requirement: '^9.3 || ^10' +core_version_requirement: '^10.3 || ^11' dependencies: - - drupal:taxonomy (>= 8.8.0) - - drupal:field (>= 8.8.0) - - drupal:user (>= 8.8.0) - - drupal:workflows (>= 8.8.0) + - drupal:field + - drupal:taxonomy + - drupal:user + - drupal:workflows - entity:entity - filefield_paths:filefield_paths - lifecycle:lifecycle + - organizations:organizations - youvo:youvo diff --git a/web/modules/custom/projects/projects/projects.module b/web/modules/custom/projects/projects/projects.module index c53f6c8e..aea1048e 100644 --- a/web/modules/custom/projects/projects/projects.module +++ b/web/modules/custom/projects/projects/projects.module @@ -54,8 +54,6 @@ function projects_entity_base_field_info(EntityTypeInterface $entity_type): arra /** * Implements hook_tokens(). - * - * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ function projects_tokens(string $type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata): array { diff --git a/web/modules/custom/projects/projects/projects.services.yml b/web/modules/custom/projects/projects/projects.services.yml index edeefa8f..5b1493c6 100644 --- a/web/modules/custom/projects/projects/projects.services.yml +++ b/web/modules/custom/projects/projects/projects.services.yml @@ -1,24 +1,9 @@ services: - project.query_access_subscriber: - class: Drupal\projects\EventSubscriber\ProjectQueryAccessSubscriber - tags: - - { name: event_subscriber } + project.lifecycle: class: Drupal\projects\Service\ProjectLifecycle arguments: [ '@entity_type.manager' ] - project.route_subscriber: - class: Drupal\projects\Routing\ProjectRouteSubscriber - tags: - - { name: event_subscriber } - project.parse_jsonapi.relationships: - class: Drupal\projects\EventSubscriber\ProjectParseJsonapiRelationshipsSubscriber - tags: - - { name: event_subscriber } - project.organization.create: - class: Drupal\projects\EventSubscriber\ProjectOrganizationCreateSubscriber - tags: - - { name: event_subscriber } logger.channel.projects: parent: logger.channel_base arguments: [ 'projects' ] @@ -44,3 +29,17 @@ services: class: Drupal\projects\EventSubscriber\Transition\ProjectSubmitSubscriber tags: - { name: event_subscriber } + + # Project action event subscribers. + projects.project_apply.event_subscriber: + class: Drupal\projects\EventSubscriber\Action\ProjectApplySubscriber + tags: + - { name: event_subscriber } + project.organization_create.event_subscriber: + class: Drupal\projects\EventSubscriber\ProjectOrganizationCreateSubscriber + tags: + - { name: event_subscriber } + project.parse_jsonapi_relationships.event_subscriber: + class: Drupal\projects\EventSubscriber\ProjectParseJsonapiRelationshipsSubscriber + tags: + - { name: event_subscriber } diff --git a/web/modules/custom/projects/projects/src/Entity/Project.php b/web/modules/custom/projects/projects/src/Entity/Project.php index 450c1efc..88c77dd8 100644 --- a/web/modules/custom/projects/projects/src/Entity/Project.php +++ b/web/modules/custom/projects/projects/src/Entity/Project.php @@ -80,6 +80,8 @@ class Project extends ContentEntityBase implements ProjectInterface { use EntityOwnerTrait; use EntityChangedTrait; + // @todo This is confusing when working with the lifecycle. Maybe replace + // with "archived" as entity key. use EntityPublishedTrait; /** @@ -117,10 +119,6 @@ public function delete(): void { public function preSave(EntityStorageInterface $storage): void { parent::preSave($storage); if ($this->isNew()) { - - $test = $this->getTranslation('de'); - $test->lifecycle(); - // Store the current organization contact in the project. We do this // because the contact of the organization may change in the future, and // we would like to know who was responsible for past projects. @@ -203,8 +201,7 @@ public function getApplicants(): array { public function setApplicants(array $applicants): static { $this->set('field_applicants', NULL); foreach ($applicants as $applicant) { - $this->get('field_applicants') - ->appendItem(['target_id' => Profile::id($applicant)]); + $this->get('field_applicants')->appendItem(['target_id' => Profile::id($applicant)]); } return $this; } @@ -213,8 +210,7 @@ public function setApplicants(array $applicants): static { * {@inheritdoc} */ public function appendApplicant(AccountInterface|int $applicant): static { - $this->get('field_applicants') - ->appendItem(['target_id' => Profile::id($applicant)]); + $this->get('field_applicants')->appendItem(['target_id' => Profile::id($applicant)]); return $this; } @@ -249,13 +245,14 @@ public function getParticipants(): array { /** * {@inheritdoc} + * + * @todo Introduce field type to associate creative and task better. */ public function setParticipants(array $participants, array $tasks = []): static { $this->set('field_participants', NULL); $this->set('field_participants_tasks', NULL); foreach ($participants as $delta => $participant) { - $this->get('field_participants') - ->appendItem(['target_id' => Profile::id($participant)]); + $this->get('field_participants')->appendItem(['target_id' => Profile::id($participant)]); $task = $tasks[$delta] ?? 'Creative'; $this->get('field_participants_tasks')->appendItem($task); } @@ -266,8 +263,7 @@ public function setParticipants(array $participants, array $tasks = []): static * {@inheritdoc} */ public function appendParticipant(AccountInterface|int $participant, string $task = 'Creative'): static { - $this->get('field_participants') - ->appendItem(['target_id' => Profile::id($participant)]); + $this->get('field_participants')->appendItem(['target_id' => Profile::id($participant)]); $this->get('field_participants_tasks')->appendItem($task); return $this; } @@ -282,8 +278,15 @@ public function isParticipant(AccountInterface|int $participant): bool { /** * {@inheritdoc} */ - public function hasParticipant(): bool { - return !empty($this->getParticipants()); + public function hasParticipant(?string $task = NULL): bool { + + if ($task === NULL) { + return !empty($this->getParticipants()); + } + + $tasks = $this->get('field_participants_tasks')->getValue(); + $someone_has_task = in_array($task, array_column($tasks, 'value'), TRUE); + return !empty($this->getParticipants()) && $someone_has_task; } /** diff --git a/web/modules/custom/projects/projects/src/Event/ProjectMediateEvent.php b/web/modules/custom/projects/projects/src/Event/ProjectMediateEvent.php index 84f295c0..196e048d 100644 --- a/web/modules/custom/projects/projects/src/Event/ProjectMediateEvent.php +++ b/web/modules/custom/projects/projects/src/Event/ProjectMediateEvent.php @@ -9,18 +9,29 @@ class ProjectMediateEvent extends ProjectEventBase { /** * An array of creatives. + * + * @var \Drupal\creatives\Entity\Creative[] */ protected array $creatives = []; /** * Gets the creatives. + * + * @return \Drupal\creatives\Entity\Creative[] + * An array of creatives. */ public function getCreatives(): array { return $this->creatives; } /** - * Sets the acreatives. + * Sets the creatives. + * + * @param \Drupal\creatives\Entity\Creative[] $creatives + * The creatives. + * + * @return $this + * The project mediate event. */ public function setCreatives(array $creatives): static { $this->creatives = $creatives; diff --git a/web/modules/custom/projects/projects/src/EventSubscriber/Transition/ProjectMediateSubscriber.php b/web/modules/custom/projects/projects/src/EventSubscriber/Transition/ProjectMediateSubscriber.php index 84171878..ad60372a 100644 --- a/web/modules/custom/projects/projects/src/EventSubscriber/Transition/ProjectMediateSubscriber.php +++ b/web/modules/custom/projects/projects/src/EventSubscriber/Transition/ProjectMediateSubscriber.php @@ -2,7 +2,6 @@ namespace Drupal\projects\EventSubscriber\Transition; -use Drupal\lifecycle\Exception\LifecycleTransitionException; use Drupal\projects\Event\ProjectMediateEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -21,25 +20,14 @@ public function onProjectMediate(ProjectMediateEvent $event): void { $project = $event->getProject(); $selected_creatives = $event->getCreatives(); + $project->setParticipants($selected_creatives); - // Check whether there are any selected creatives. - if (empty($selected_creatives)) { - throw new LifecycleTransitionException('Unable to mediate project without selecting creatives.'); - } - - // Get project applicants and check if selected creatives are applicable. - $applicants = $project->getApplicants(); - if (count(array_intersect($selected_creatives, $applicants)) !== count($selected_creatives)) { - throw new LifecycleTransitionException('Some selected creatives did not apply for this project.'); + if ($manager = $project->getOwner()->getManager()) { + $project->appendParticipant($manager, 'Manager'); } - // Transition project. $project->lifecycle()->mediate(); - $project->setParticipants($selected_creatives); $project->setPromoted(FALSE); - if ($manager = $project->getOwner()->getManager()) { - $project->appendParticipant($manager, 'Manager'); - } $project->save(); } diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectActionResourceBase.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectActionResourceBase.php index 09cdc847..c624981d 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectActionResourceBase.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectActionResourceBase.php @@ -2,7 +2,6 @@ namespace Drupal\projects\Plugin\rest\resource; -use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountProxyInterface; @@ -41,8 +40,6 @@ public static function create(ContainerInterface $container, ...$defaults) { /** * Handles custom access logic for the resource. */ - public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { - return AccessResult::neutral(); - } + abstract public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface; } diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectApplyResource.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectApplyResource.php index ae9ff239..351fd99e 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectApplyResource.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectApplyResource.php @@ -5,7 +5,9 @@ use Drupal\Component\Serialization\Json; use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\creatives\Entity\Creative; use Drupal\projects\Event\ProjectApplyEvent; use Drupal\projects\ProjectInterface; use Drupal\rest\ModifiedResourceResponse; @@ -31,24 +33,31 @@ class ProjectApplyResource extends ProjectActionResourceBase { */ public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { - $access_result = AccessResult::allowed(); + // The user requires the permission to do this action. + $permission = 'restful post project:apply'; + $access_result = AccessResult::allowedIfHasPermission($account, $permission); - // The project may not be open to apply. - if (!$project->isPublished() || !$project->lifecycle()->isOpen()) { - $access_result = AccessResult::forbidden('The project is not open for applications.'); + // The resource should define project-dependent access conditions. + $project_condition = $project->isPublished() && $project->lifecycle()->isOpen(); + $access_project = AccessResult::allowedIf($project_condition) + ->addCacheableDependency($project); + if ($access_project instanceof AccessResultReasonInterface) { + $access_project->setReason('The project conditions for this application are not met.'); } + $access_result = $access_result->andIf($access_project); - // The user may not be allowed to apply to this project. - if (!Profile::isCreative($account) || $project->getOwner()->isManager($account)) { - $access_result = AccessResult::forbidden('The user is not allowed to apply to this project.'); + // The resource should define applicant-dependent access conditions. We + // assume that the user type can not change. + $organization = $project->getOwner(); + $applicant_condition = Profile::isCreative($account) && !$organization->isManager($account) && !$project->isApplicant($account); + $access_applicant = AccessResult::allowedIf($applicant_condition) + ->addCacheableDependency($organization) + ->addCacheableDependency($project); + if ($access_applicant instanceof AccessResultReasonInterface) { + $access_applicant->setReason('The applicant conditions for this application are not met. The creative may already applied.'); } - // The user maybe already applied to this project. - if ($project->isApplicant($account)) { - $access_result = AccessResult::forbidden('The user already applied to this project.'); - } - - return $access_result->addCacheableDependency($project)->cachePerUser(); + return $access_result->andIf($access_applicant); } /** @@ -64,9 +73,15 @@ public function get(): ResourceResponseInterface { public function post(ProjectInterface $project, Request $request): ResourceResponseInterface { $content = Json::decode($request->getContent()); - /** @var \Drupal\creatives\Entity\Creative $applicant */ $applicant = $this->currentUser->getAccount(); + // Safeguard against a current user that is not a creative. + if (!$applicant instanceof Creative) { + // @codeCoverageIgnoreStart + return new ModifiedResourceResponse('The application is not possible for the current user.'); + // @codeCoverageIgnoreEnd + } + try { $event = new ProjectApplyEvent($project, $applicant); $event->setMessage($content['message'] ?? ''); diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectCompleteResource.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectCompleteResource.php index ef6ba495..98ff56c6 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectCompleteResource.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectCompleteResource.php @@ -6,11 +6,17 @@ use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Component\Serialization\Json; use Drupal\Component\Uuid\Uuid; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Session\AccountInterface; use Drupal\file\FileInterface; use Drupal\lifecycle\Exception\LifecycleTransitionException; +use Drupal\lifecycle\WorkflowPermissions; use Drupal\projects\Event\ProjectCompleteEvent; use Drupal\projects\ProjectInterface; +use Drupal\projects\ProjectTransition; +use Drupal\projects\Service\ProjectLifecycle; use Drupal\rest\ModifiedResourceResponse; use Drupal\rest\ResourceResponseInterface; use Symfony\Component\HttpFoundation\Request; @@ -30,15 +36,33 @@ */ class ProjectCompleteResource extends ProjectTransitionResourceBase { - protected const TRANSITION = 'complete'; - /** - * {@inheritdoc} + * Handles custom access logic for the resource. */ - protected static function projectAccessCondition(AccountInterface $account, ProjectInterface $project): bool { - return $project->isAuthor($account) || - $project->isParticipant($account) || - $project->getOwner()->isManager($account); + public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { + + // The user may be permitted to bypass access control. + $workflow_id = ProjectLifecycle::WORKFLOW_ID; + $bybass_permission = WorkflowPermissions::bypassTransition($workflow_id); + if ($account->hasPermission($bybass_permission)) { + return AccessResult::allowed()->cachePerPermissions(); + } + + // The user requires the permission to initiate this transition. + $permission = WorkflowPermissions::useTransition($workflow_id, ProjectTransition::COMPLETE->value); + $access_result = AccessResult::allowedIfHasPermission($account, $permission); + + // The resource should define project-dependent access conditions. + $organization = $project->getOwner(); + $project_condition = $project->isPublished() && ($project->isAuthor($account) || $project->isParticipant($account) || $organization->isManager($account)); + $access_project = AccessResult::allowedIf($project_condition) + ->addCacheableDependency($project) + ->addCacheableDependency($organization); + if ($access_project instanceof AccessResultReasonInterface) { + $access_project->setReason('The project conditions for this transition are not met.'); + } + + return $access_result->andIf($access_project); } /** @@ -46,7 +70,7 @@ protected static function projectAccessCondition(AccountInterface $account, Proj */ public function post(ProjectInterface $project, Request $request): ResourceResponseInterface { - $content = Json::decode($request->getContent()); + $content = Json::decode($request->getContent()) ?? []; $this->validateRequestContent($content); try { @@ -69,6 +93,8 @@ public function post(ProjectInterface $project, Request $request): ResourceRespo /** * Validates the request body. + * + * @codeCoverageIgnore */ protected function validateRequestContent(array $content): void { $results = $content['results'] ?? []; diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectMediateResource.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectMediateResource.php index 7ceb16bc..bbac1cef 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectMediateResource.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectMediateResource.php @@ -6,15 +6,22 @@ use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Component\Serialization\Json; use Drupal\Component\Uuid\Uuid; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\creatives\Entity\Creative; use Drupal\lifecycle\Exception\LifecycleTransitionException; +use Drupal\lifecycle\WorkflowPermissions; use Drupal\projects\Event\ProjectMediateEvent; use Drupal\projects\ProjectInterface; +use Drupal\projects\ProjectTransition; +use Drupal\projects\Service\ProjectLifecycle; use Drupal\rest\ResourceResponse; use Drupal\rest\ResourceResponseInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; /** * Provides project mediate resource. @@ -29,13 +36,33 @@ */ class ProjectMediateResource extends ProjectTransitionResourceBase { - protected const TRANSITION = 'mediate'; - /** * {@inheritdoc} */ - protected static function projectAccessCondition(AccountInterface $account, ProjectInterface $project): bool { - return $project->isAuthor($account) || $project->getOwner()->isManager($account); + public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { + + // The user may be permitted to bypass access control. + $workflow_id = ProjectLifecycle::WORKFLOW_ID; + $bybass_permission = WorkflowPermissions::bypassTransition($workflow_id); + if ($account->hasPermission($bybass_permission)) { + return AccessResult::allowed()->cachePerPermissions(); + } + + // The user requires the permission to initiate this transition. + $permission = WorkflowPermissions::useTransition($workflow_id, ProjectTransition::MEDIATE->value); + $access_result = AccessResult::allowedIfHasPermission($account, $permission); + + // The resource should define project-dependent access conditions. + $organization = $project->getOwner(); + $project_condition = $project->isPublished() && ($project->isAuthor($account) || $organization->isManager($account)); + $access_project = AccessResult::allowedIf($project_condition) + ->addCacheableDependency($project) + ->addCacheableDependency($organization); + if ($access_project instanceof AccessResultReasonInterface) { + $access_project->setReason('The project conditions for this transition are not met.'); + } + + return $access_result->andIf($access_project); } /** @@ -81,26 +108,28 @@ public function get(ProjectInterface $project): ResourceResponseInterface { */ public function post(ProjectInterface $project, Request $request): ResourceResponseInterface { - $content = Json::decode($request->getContent()); + $content = Json::decode($request->getContent()) ?? []; $this->validateRequestContent($content); try { $selected_creatives = $this->loadSelectedCreatives($content); $event = new ProjectMediateEvent($project); $event->setCreatives($selected_creatives); - $this->eventDispatcher->dispatch(new ProjectMediateEvent($project)); + $this->eventDispatcher->dispatch($event); } catch (LifecycleTransitionException | InvalidPluginDefinitionException | PluginNotFoundException) { - throw new UnprocessableEntityHttpException('Could not mediate project.'); + throw new ConflictHttpException('Project can not be mediated.'); } catch (\Throwable) { } - return new ResourceResponse('Project was mediated successfully.'); + return new ResourceResponse('Project mediated.'); } /** * Validates the request content. + * + * @codeCoverageIgnore */ protected function validateRequestContent(array $content): void { @@ -141,6 +170,7 @@ protected function validateRequestContent(array $content): void { * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function loadSelectedCreatives(array $content): array { + $selected_creatives_uuids = array_unique($content['selected_creatives'] ?? []); $selected_creatives_ids = $this->entityTypeManager ->getStorage('user') @@ -149,10 +179,21 @@ protected function loadSelectedCreatives(array $content): array { ->condition('uuid', $selected_creatives_uuids, 'IN') ->execute(); $selected_creatives_ids = array_map('intval', $selected_creatives_ids); + /** @var \Drupal\creatives\Entity\Creative[] $selected_creatives */ $selected_creatives = $this->entityTypeManager ->getStorage('user') ->loadMultiple($selected_creatives_ids); + + // Safeguard against unintentionally loaded users that are not creatives. + foreach ($selected_creatives as $uid => $creative) { + if (!$creative instanceof Creative) { + // @codeCoverageIgnoreStart + unset($selected_creatives[$uid]); + // @codeCoverageIgnoreEnd + } + } + return $selected_creatives; } diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectNotifyResource.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectNotifyResource.php index c2ade5b7..66ea19a1 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectNotifyResource.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectNotifyResource.php @@ -4,6 +4,7 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Session\AccountInterface; use Drupal\projects\Entity\Project; use Drupal\projects\Event\ProjectInviteEvent; @@ -30,37 +31,41 @@ class ProjectNotifyResource extends ProjectActionResourceBase { */ public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { - $access_result = AccessResult::allowed(); - - // Supervisors may bypass the access check. + // The user may be permitted to bypass access control. + // @todo Add specific permission. if (in_array('supervisor', $account->getRoles(), TRUE)) { - return $access_result->addCacheableDependency($project)->cachePerUser(); + return AccessResult::allowed()->addCacheContexts(['user.roles:supervisor'])->cachePerUser(); } - // The project may not be open to apply. - if (!$project->isPublished() || !$project->lifecycle()->isDraft()) { - $access_result = AccessResult::forbidden('The organization can not be notified for this project.'); - } + // The user requires the permission to do this action. + $permission = 'restful post project:notify'; + $access_result = AccessResult::allowedIfHasPermission($account, $permission); - // The project notification may only be triggered by its manager. - if (!$project->getOwner()->isManager($account)) { - $access_result = AccessResult::forbidden('The user does not manage the project.'); + // The resource should define project-dependent access conditions. + $organization = $project->getOwner(); + $project_condition = $project->isPublished() && $project->lifecycle()->isDraft() && $organization->isManager($account); + $access_project = AccessResult::allowedIf($project_condition) + ->addCacheableDependency($organization) + ->addCacheableDependency($project); + if ($access_project instanceof AccessResultReasonInterface) { + $access_project->setReason('The project conditions for this action are not met.'); } - return $access_result->addCacheableDependency($project)->cachePerUser(); + return $access_result->andIf($access_project); } /** * Responds to POST requests. */ public function post(Project $project): ResourceResponseInterface { + if ($project->getOwner()->hasRoleProspect()) { $this->eventDispatcher->dispatch(new ProjectInviteEvent($project)); + return new ModifiedResourceResponse('The organization was invited.'); } - else { - $this->eventDispatcher->dispatch(new ProjectNotifyEvent($project)); - } - return new ModifiedResourceResponse(); + + $this->eventDispatcher->dispatch(new ProjectNotifyEvent($project)); + return new ModifiedResourceResponse('The organization was notified.'); } } diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectPublishResource.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectPublishResource.php index 057c694a..ebcfee50 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectPublishResource.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectPublishResource.php @@ -2,10 +2,16 @@ namespace Drupal\projects\Plugin\rest\resource; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Session\AccountInterface; use Drupal\lifecycle\Exception\LifecycleTransitionException; +use Drupal\lifecycle\WorkflowPermissions; use Drupal\projects\Event\ProjectPublishEvent; use Drupal\projects\ProjectInterface; +use Drupal\projects\ProjectTransition; +use Drupal\projects\Service\ProjectLifecycle; use Drupal\rest\ModifiedResourceResponse; use Drupal\rest\ResourceResponseInterface; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; @@ -23,13 +29,31 @@ */ class ProjectPublishResource extends ProjectTransitionResourceBase { - protected const TRANSITION = 'publish'; - /** * {@inheritdoc} */ - protected static function projectAccessCondition(AccountInterface $account, ProjectInterface $project): bool { - return $project->getOwner()->isManager($account); + public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { + + // The user may be permitted to bypass access control. + $workflow_id = ProjectLifecycle::WORKFLOW_ID; + $bybass_permission = WorkflowPermissions::bypassTransition($workflow_id); + if ($account->hasPermission($bybass_permission)) { + return AccessResult::allowed()->cachePerPermissions(); + } + + // The user requires the permission to initiate this transition. + $permission = WorkflowPermissions::useTransition($workflow_id, ProjectTransition::PUBLISH->value); + $access_result = AccessResult::allowedIfHasPermission($account, $permission); + + // The resource should define project-dependent access conditions. + $project_condition = $project->isPublished() && $project->getOwner()->isManager($account); + $access_project = AccessResult::allowedIf($project_condition) + ->addCacheableDependency($project); + if ($access_project instanceof AccessResultReasonInterface) { + $access_project->setReason('The project conditions for this transition are not met.'); + } + + return $access_result->andIf($access_project); } /** diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectResetResource.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectResetResource.php index fda10555..1f6977ee 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectResetResource.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectResetResource.php @@ -2,10 +2,14 @@ namespace Drupal\projects\Plugin\rest\resource; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Session\AccountInterface; use Drupal\lifecycle\Exception\LifecycleTransitionException; +use Drupal\lifecycle\WorkflowPermissions; use Drupal\projects\Event\ProjectResetEvent; use Drupal\projects\ProjectInterface; +use Drupal\projects\Service\ProjectLifecycle; use Drupal\rest\ModifiedResourceResponse; use Drupal\rest\ResourceResponseInterface; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; @@ -23,13 +27,14 @@ */ class ProjectResetResource extends ProjectTransitionResourceBase { - protected const TRANSITION = 'reset'; - /** * {@inheritdoc} */ - protected static function projectAccessCondition(AccountInterface $account, ProjectInterface $project): bool { - return FALSE; + public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { + // This resource may only be permitted for users with access control bypass. + $workflow_id = ProjectLifecycle::WORKFLOW_ID; + $bybass_permission = WorkflowPermissions::bypassTransition($workflow_id); + return AccessResult::allowedIfHasPermission($account, $bybass_permission); } /** @@ -39,9 +44,12 @@ public function post(ProjectInterface $project): ResourceResponseInterface { try { $this->eventDispatcher->dispatch(new ProjectResetEvent($project)); } + // @codeCoverageIgnoreStart + // This exception is not possible with the current configuration. catch (LifecycleTransitionException) { throw new ConflictHttpException('Project can not be reset.'); } + // @codeCoverageIgnoreEnd catch (\Throwable) { } return new ModifiedResourceResponse('Project reset.'); diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectSubmitResource.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectSubmitResource.php index a8573d48..932aa2ff 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectSubmitResource.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectSubmitResource.php @@ -2,10 +2,16 @@ namespace Drupal\projects\Plugin\rest\resource; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Session\AccountInterface; use Drupal\lifecycle\Exception\LifecycleTransitionException; +use Drupal\lifecycle\WorkflowPermissions; use Drupal\projects\Event\ProjectSubmitEvent; use Drupal\projects\ProjectInterface; +use Drupal\projects\ProjectTransition; +use Drupal\projects\Service\ProjectLifecycle; use Drupal\rest\ModifiedResourceResponse; use Drupal\rest\ResourceResponseInterface; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; @@ -23,13 +29,31 @@ */ class ProjectSubmitResource extends ProjectTransitionResourceBase { - protected const TRANSITION = 'submit'; - /** * {@inheritdoc} */ - protected static function projectAccessCondition(AccountInterface $account, ProjectInterface $project): bool { - return $project->isAuthor($account); + public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { + + // The user may be permitted to bypass access control. + $workflow_id = ProjectLifecycle::WORKFLOW_ID; + $bybass_permission = WorkflowPermissions::bypassTransition($workflow_id); + if ($account->hasPermission($bybass_permission)) { + return AccessResult::allowed()->cachePerPermissions(); + } + + // The user requires the permission to initiate this transition. + $permission = WorkflowPermissions::useTransition($workflow_id, ProjectTransition::SUBMIT->value); + $access_result = AccessResult::allowedIfHasPermission($account, $permission); + + // The resource should define project-dependent access conditions. + $project_condition = $project->isPublished() && $project->isAuthor($account); + $access_project = AccessResult::allowedIf($project_condition) + ->addCacheableDependency($project); + if ($access_project instanceof AccessResultReasonInterface) { + $access_project->setReason('The project conditions for this transition are not met.'); + } + + return $access_result->andIf($access_project); } /** diff --git a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectTransitionResourceBase.php b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectTransitionResourceBase.php index 9ddf2c70..af294241 100644 --- a/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectTransitionResourceBase.php +++ b/web/modules/custom/projects/projects/src/Plugin/rest/resource/ProjectTransitionResourceBase.php @@ -2,29 +2,21 @@ namespace Drupal\projects\Plugin\rest\resource; -use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\lifecycle\WorkflowPermissions; use Drupal\projects\ProjectInterface; -use Drupal\projects\ProjectTransition; -use Drupal\projects\Service\ProjectLifecycle; use Drupal\rest\Plugin\ResourceBase; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Provides base class for project transition resources. - * - * @todo Use enums for transition constant starting with PHP 8.2. */ abstract class ProjectTransitionResourceBase extends ResourceBase { use ProjectResourceRoutesTrait; - protected const TRANSITION = 'undefined'; - /** * The entity type manager. */ @@ -48,40 +40,6 @@ public static function create(ContainerInterface $container, ...$defaults) { /** * Handles custom access logic for the resource. */ - public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface { - - $workflow_id = ProjectLifecycle::WORKFLOW_ID; - $transition = static::TRANSITION; - $access_result = AccessResult::allowed(); - - // The user may be permitted to bypass access control. - $bybass_permission = WorkflowPermissions::bypassTransition($workflow_id); - if ($account->hasPermission($bybass_permission)) { - return $access_result->cachePerPermissions(); - } - - // The resource should define project-dependent access conditions. - if (!static::projectAccessCondition($account, $project)) { - $access_result = AccessResult::forbidden('The project conditions for this transition are not met.'); - } - - // The project should be able to perform the given transition. - if (!$project->isPublished() || !$project->lifecycle()->canTransition(ProjectTransition::from($transition))) { - $access_result = AccessResult::forbidden('The project is not ready for this transition.'); - } - - // The user may not have the permission to initiate this transition. - $permission = WorkflowPermissions::useTransition($workflow_id, $transition); - if (!$account->hasPermission($permission)) { - $access_result = AccessResult::forbidden('The user is not allowed to initiate this transition.'); - } - - return $access_result->addCacheableDependency($project)->cachePerUser(); - } - - /** - * Defines project-dependent access condition. - */ - abstract protected static function projectAccessCondition(AccountInterface $account, ProjectInterface $project): bool; + abstract public static function access(AccountInterface $account, ProjectInterface $project): AccessResultInterface; } diff --git a/web/modules/custom/projects/projects/src/ProjectInterface.php b/web/modules/custom/projects/projects/src/ProjectInterface.php index c2d7fc4c..3172a885 100644 --- a/web/modules/custom/projects/projects/src/ProjectInterface.php +++ b/web/modules/custom/projects/projects/src/ProjectInterface.php @@ -89,8 +89,14 @@ public function isParticipant(AccountInterface|int $participant): bool; /** * Checks whether the project has a participant. + * + * @param string|null $task + * Check whether the project has a participant with the given task. + * + * @return bool + * TRUE if participant found (with given task). */ - public function hasParticipant(): bool; + public function hasParticipant(?string $task = NULL): bool; /** * Gets the participants array keyed by UID. diff --git a/web/modules/custom/projects/projects/src/Routing/ProjectRouteSubscriber.php b/web/modules/custom/projects/projects/src/Routing/ProjectRouteSubscriber.php deleted file mode 100644 index 7d29ba4c..00000000 --- a/web/modules/custom/projects/projects/src/Routing/ProjectRouteSubscriber.php +++ /dev/null @@ -1,23 +0,0 @@ -get('system.admin_content')) { - $route->setDefault('_title', 'Projects'); - } - } - -} diff --git a/web/modules/custom/projects/projects/src/Service/ProjectLifecycle.php b/web/modules/custom/projects/projects/src/Service/ProjectLifecycle.php index 81569dbf..b61a8fb5 100644 --- a/web/modules/custom/projects/projects/src/Service/ProjectLifecycle.php +++ b/web/modules/custom/projects/projects/src/Service/ProjectLifecycle.php @@ -91,19 +91,6 @@ public function isCompleted(): bool { return $this->getState() === ProjectState::COMPLETED; } - /** - * {@inheritdoc} - * - * @todo Check whether this can be protected. - */ - public function canTransition(ProjectTransition $transition): bool { - if ($transition === ProjectTransition::MEDIATE) { - return $this->project()->hasApplicant() && - $this->hasTransition($transition); - } - return $this->hasTransition($transition); - } - /** * Submits the project. */ @@ -142,18 +129,31 @@ public function reset(): bool { /** * Abstraction of forward transition flow check. */ - protected function hasTransition(ProjectTransition $transition): bool { + protected function hasTransition(ProjectState $from, ProjectState $to): bool { /** @var \Drupal\workflows\WorkflowInterface $workflow */ - $workflow = $this->entityTypeManager->getStorage('workflow')->load(static::WORKFLOW_ID); - return $workflow->getTypePlugin()->hasTransition($transition->value); + $workflow = $this->entityTypeManager + ->getStorage('workflow') + ->load(static::WORKFLOW_ID); + return $workflow->getTypePlugin() + ->hasTransitionFromStateToState($from->value, $to->value); + } + + /** + * Checks if the project can perform the given transition. + */ + protected function canTransition(ProjectTransition $transition, ProjectState $from, ProjectState $to): bool { + if ($transition === ProjectTransition::MEDIATE || $transition === ProjectTransition::COMPLETE) { + return $this->project()->hasParticipant('Creative') && $this->hasTransition($from, $to); + } + return $this->hasTransition($from, $to); } /** * Sets new project state for given transition. */ protected function doTransition(ProjectTransition $transition): bool { - if ($this->canTransition($transition)) { - $new_state = $this->getSuccessorFromTransition($transition); + $new_state = $this->getSuccessorFromTransition($transition); + if ($this->canTransition($transition, $this->getState(), $new_state)) { $this->project()->set(static::LIFECYCLE_FIELD, $new_state->value); return TRUE; } diff --git a/web/modules/custom/projects/projects/src/Service/ProjectLifecycleInterface.php b/web/modules/custom/projects/projects/src/Service/ProjectLifecycleInterface.php index 023b0870..c436b9e6 100644 --- a/web/modules/custom/projects/projects/src/Service/ProjectLifecycleInterface.php +++ b/web/modules/custom/projects/projects/src/Service/ProjectLifecycleInterface.php @@ -3,7 +3,6 @@ namespace Drupal\projects\Service; use Drupal\projects\ProjectInterface; -use Drupal\projects\ProjectTransition; /** * Provides methods to manage the workflow of a project. @@ -45,11 +44,6 @@ public function isOngoing(): bool; */ public function isCompleted(): bool; - /** - * Checks if the project can transition by transition label. - */ - public function canTransition(ProjectTransition $transition): bool; - /** * Submits the project. */ diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_applicants.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_applicants.yml new file mode 100644 index 00000000..d5f6fe57 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_applicants.yml @@ -0,0 +1,38 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.project.field_applicants + - user_bundle.user_type.user +id: project.project.field_applicants +field_name: field_applicants +entity_type: project +bundle: project +label: Applicants +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:user' + handler_settings: + target_bundles: + user: user + sort: + field: _none + direction: ASC + auto_create: false + auto_create_bundle: '' + filter: + type: role + role: + creative: creative + administrator: '0' + organization: '0' + observer: '0' + editor: '0' + manager: '0' + supervisor: '0' + include_anonymous: false +field_type: entity_reference diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_contact.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_contact.yml new file mode 100644 index 00000000..551307a3 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_contact.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.project.field_contact + module: + - projects +id: project.project.field_contact +field_name: field_contact +entity_type: project +bundle: project +label: Contact +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_lifecycle.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_lifecycle.yml new file mode 100644 index 00000000..97424221 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_lifecycle.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.project.field_lifecycle + module: + - lifecycle +id: project.project.field_lifecycle +field_name: field_lifecycle +entity_type: project +bundle: project +label: Lifecycle +description: '' +required: false +translatable: false +default_value: + - + value: draft +default_value_callback: '' +settings: { } +field_type: lifecycle_item diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_participants.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_participants.yml new file mode 100644 index 00000000..1de611b3 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_participants.yml @@ -0,0 +1,38 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.project.field_participants + - user_bundle.user_type.user +id: project.project.field_participants +field_name: field_participants +entity_type: project +bundle: project +label: Participants +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:user' + handler_settings: + target_bundles: + user: user + sort: + field: _none + direction: ASC + auto_create: false + auto_create_bundle: '' + filter: + type: role + role: + creative: creative + administrator: '0' + organization: '0' + observer: '0' + editor: '0' + manager: '0' + supervisor: '0' + include_anonymous: false +field_type: entity_reference diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_participants_tasks.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_participants_tasks.yml new file mode 100644 index 00000000..a59727a8 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_participants_tasks.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.project.field_participants_tasks + module: + - options +id: project.project.field_participants_tasks +field_name: field_participants_tasks +entity_type: project +bundle: project +label: 'Participants Tasks' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: list_string diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.organization.field_contact.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.organization.field_contact.yml new file mode 100644 index 00000000..1fc3b87c --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.organization.field_contact.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.user.field_contact + - user_bundle.user_type.organization +id: user.organization.field_contact +field_name: field_contact +entity_type: user +bundle: organization +label: Contact +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.organization.field_manager.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.organization.field_manager.yml new file mode 100644 index 00000000..e66d9251 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.organization.field_manager.yml @@ -0,0 +1,39 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.user.field_manager + - user_bundle.user_type.organization + - user_bundle.user_type.user +id: user.organization.field_manager +field_name: field_manager +entity_type: user +bundle: organization +label: Manager +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:user' + handler_settings: + target_bundles: + user: user + sort: + field: _none + direction: ASC + auto_create: false + auto_create_bundle: '' + filter: + type: role + role: + manager: manager + supervisor: supervisor + administrator: '0' + creative: '0' + organization: '0' + observer: '0' + editor: '0' + include_anonymous: false +field_type: entity_reference diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.user.field_phone.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.user.field_phone.yml new file mode 100644 index 00000000..3ea0b140 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.user.user.field_phone.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.user.field_phone + - user_bundle.user_type.user +id: user.user.field_phone +field_name: field_phone +entity_type: user +bundle: user +label: Phone +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_applicants.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_applicants.yml new file mode 100644 index 00000000..7ff1b32b --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_applicants.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - projects + - user +id: project.field_applicants +field_name: field_applicants +entity_type: project +type: entity_reference +settings: + target_type: user +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_contact.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_contact.yml new file mode 100644 index 00000000..b39b3ccc --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_contact.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - projects +id: project.field_contact +field_name: field_contact +entity_type: project +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_lifecycle.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_lifecycle.yml new file mode 100644 index 00000000..a7c5c10a --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_lifecycle.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + config: + - workflows.workflow.project_lifecycle + module: + - projects + - lifecycle +id: project.field_lifecycle +field_name: field_lifecycle +entity_type: project +type: lifecycle_item +settings: + workflow: project_lifecycle +module: lifecycle +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_participants.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_participants.yml new file mode 100644 index 00000000..6ce1d653 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_participants.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - projects + - user +id: project.field_participants +field_name: field_participants +entity_type: project +type: entity_reference +settings: + target_type: user +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_participants_tasks.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_participants_tasks.yml new file mode 100644 index 00000000..d454a380 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_participants_tasks.yml @@ -0,0 +1,26 @@ +langcode: en +status: true +dependencies: + module: + - projects + - options +id: project.field_participants_tasks +field_name: field_participants_tasks +entity_type: project +type: list_string +settings: + allowed_values: + - + value: Creative + label: Creative + - + value: Manager + label: Manager + allowed_values_function: '' +module: options +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_contact.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_contact.yml new file mode 100644 index 00000000..989b5748 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_contact.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - user +id: user.field_contact +field_name: field_contact +entity_type: user +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_manager.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_manager.yml new file mode 100644 index 00000000..0edd4812 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_manager.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - user +id: user.field_manager +field_name: field_manager +entity_type: user +type: entity_reference +settings: + target_type: user +module: core +locked: false +cardinality: 1 +translatable: false +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_phone.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_phone.yml new file mode 100644 index 00000000..db9f1dba --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.user.field_phone.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - user +id: user.field_phone +field_name: field_phone +entity_type: user +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/user_bundle.user_type.organization.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/user_bundle.user_type.organization.yml new file mode 100644 index 00000000..bd11aaef --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/user_bundle.user_type.organization.yml @@ -0,0 +1,6 @@ +langcode: en +status: true +dependencies: { } +id: organization +label: Organization +description: 'An organization.' diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/user_bundle.user_type.user.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/user_bundle.user_type.user.yml new file mode 100644 index 00000000..3ddd2b21 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/user_bundle.user_type.user.yml @@ -0,0 +1,6 @@ +langcode: en +status: true +dependencies: { } +id: user +label: Creative +description: 'A user of the site.' diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/workflows.workflow.project_lifecycle.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/workflows.workflow.project_lifecycle.yml new file mode 100644 index 00000000..962de849 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/workflows.workflow.project_lifecycle.yml @@ -0,0 +1,90 @@ +langcode: en +status: true +dependencies: + module: + - lifecycle +id: project_lifecycle +label: 'Project Lifecycle' +type: lifecycle +type_settings: + states: + draft: + label: Draft + weight: 0 + completed: + label: Completed + weight: 4 + ongoing: + label: Ongoing + weight: 3 + open: + label: Open + weight: 2 + pending: + label: Pending + weight: 1 + transitions: + remain_draft: + label: 'Project Remain Draft' + from: + - draft + to: draft + weight: 0 + complete: + label: 'Project Complete' + from: + - ongoing + to: completed + weight: 7 + mediate: + label: 'Project Mediate' + from: + - open + to: ongoing + weight: 5 + publish: + label: 'Project Publish' + from: + - pending + to: open + weight: 3 + remain_completed: + label: 'Project Remain Completed' + from: + - completed + to: completed + weight: 8 + remain_ongoing: + label: 'Project Remain Ongoing' + from: + - ongoing + to: ongoing + weight: 6 + remain_open: + label: 'Project Remain Open' + from: + - open + to: open + weight: 4 + remain_pending: + label: 'Project Remain Pending' + from: + - pending + to: pending + weight: 2 + reset: + label: 'Project Reset' + from: + - completed + - ongoing + - open + - pending + to: draft + weight: 9 + submit: + label: 'Project Submit' + from: + - draft + to: pending + weight: 1 + initial_state: draft diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/projects_lifecycle_test.info.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/projects_lifecycle_test.info.yml new file mode 100644 index 00000000..b31fefe4 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/projects_lifecycle_test.info.yml @@ -0,0 +1,6 @@ +name: Projects Lifecycle Test +type: module +description: Adds project lifecycle for testing. +core_version_requirement: ^10.3 || ^11 +dependencies: + - field:field diff --git a/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.field.project_result.project_result.field_files.yml b/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.field.project_result.project_result.field_files.yml new file mode 100644 index 00000000..b4fad66e --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.field.project_result.project_result.field_files.yml @@ -0,0 +1,26 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.project_result.field_files + module: + - projects + - youvo +id: project_result.project_result.field_files +field_name: field_files +entity_type: project_result +bundle: project_result +label: 'Result Files' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:file' + handler_settings: { } + file_directory: '' + file_extensions: 'jpg jpeg gif png pdf' + max_filesize: '10 MB' + description_field: false +field_type: file diff --git a/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.field.project_result.project_result.field_hyperlinks.yml b/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.field.project_result.project_result.field_hyperlinks.yml new file mode 100644 index 00000000..02ed9b60 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.field.project_result.project_result.field_hyperlinks.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.project_result.field_hyperlinks + module: + - projects +id: project_result.project_result.field_hyperlinks +field_name: field_hyperlinks +entity_type: project_result +bundle: project_result +label: Hyperlinks +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.storage.project_result.field_files.yml b/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.storage.project_result.field_files.yml new file mode 100644 index 00000000..d35a41e4 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.storage.project_result.field_files.yml @@ -0,0 +1,23 @@ +langcode: en +status: true +dependencies: + module: + - file + - projects + - youvo +id: project_result.field_files +field_name: field_files +entity_type: project_result +type: file +settings: + target_type: file + display_field: false + display_default: false + uri_scheme: public +module: youvo +locked: false +cardinality: 3 +translatable: false +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.storage.project_result.field_hyperlinks.yml b/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.storage.project_result.field_hyperlinks.yml new file mode 100644 index 00000000..0ba8e0de --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_result_test/config/install/field.storage.project_result.field_hyperlinks.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - projects +id: project_result.field_hyperlinks +field_name: field_hyperlinks +entity_type: project_result +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false +module: youvo +locked: false +cardinality: -1 +translatable: false +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/modules/projects_result_test/projects_result_test.info.yml b/web/modules/custom/projects/projects/tests/modules/projects_result_test/projects_result_test.info.yml new file mode 100644 index 00000000..4b6711e8 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_result_test/projects_result_test.info.yml @@ -0,0 +1,4 @@ +name: Projects Result Test +type: module +description: Adds project results for testing. +core_version_requirement: ^10.3 || ^11 diff --git a/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectApplyResourceTest.php b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectApplyResourceTest.php new file mode 100644 index 00000000..3baad08e --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectApplyResourceTest.php @@ -0,0 +1,163 @@ +createProject(ProjectState::OPEN); + $creative = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/apply'; + $request = Request::create($path, 'GET'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"The user may apply to the project."', $response->getContent()); + } + + /** + * Tests the project apply resource - standard POST workflow. + * + * @covers ::post + */ + public function testProjectApply(): void { + + $project = $this->createProject(ProjectState::OPEN); + $creative = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/apply'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Application completed."', $response->getContent()); + } + + /** + * Tests the project apply resource - already applied. + * + * @covers ::access + */ + public function testProjectApplyAlreadyApplied(): void { + + $project = $this->createProject(ProjectState::OPEN); + $creative = $this->createCreative(); + $project->appendApplicant($creative); + $project->save(); + + $path = '/api/projects/' . $project->uuid() . '/apply'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The applicant conditions for this application are not met. The creative may already applied."}', $response->getContent()); + } + + /** + * Tests the project apply resource - manager. + * + * @covers ::access + */ + public function testProjectApplyManager(): void { + + $project = $this->createProject(ProjectState::OPEN); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/apply'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The applicant conditions for this application are not met. The creative may already applied."}', $response->getContent()); + } + + /** + * Tests the project apply resource - not open. + * + * @covers ::access + */ + public function testProjectApplyNotOpen(): void { + + $project = $this->createProject(ProjectState::PENDING); + $creative = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/apply'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this application are not met."}', $response->getContent()); + } + + /** + * Tests the project apply resource - not published (status). + * + * @covers ::access + */ + public function testProjectApplyNotPublished(): void { + + $project = $this->createProject(ProjectState::OPEN); + $project->setUnpublished(); + $project->save(); + $creative = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/apply'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this application are not met."}', $response->getContent()); + } + + /** + * Tests the project apply resource - no permission. + * + * @covers ::access + */ + public function testProjectApplyNoPermission(): void { + + $project = $this->createProject(ProjectState::OPEN); + $organization = $this->createOrganization(); + + $path = '/api/projects/' . $project->uuid() . '/apply'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The \u0027restful post project:apply\u0027 permission is required."}', $response->getContent()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectCompleteResourceTest.php b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectCompleteResourceTest.php new file mode 100644 index 00000000..f6ce77ab --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectCompleteResourceTest.php @@ -0,0 +1,259 @@ +createProject(ProjectState::ONGOING); + $organization = $project->getOwner(); + $participant = $this->createCreative(); + $project->appendParticipant($participant); + $project->save(); + + $file = File::create(['uri' => $this->randomString()]); + $file->save(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $body = [ + 'comment' => $this->randomString(), + 'results' => [ + [ + 'type' => 'file', + 'value' => $file->uuid(), + 'description' => $this->randomString(), + ], + [ + 'type' => 'file', + 'value' => 'a73b8b10-061c-4f0a-938d-118fcedba242', + 'description' => 'File not found', + ], + [ + 'type' => 'link', + 'value' => $this->randomString(), + 'description' => $this->randomString(), + ], + ], + ]; + $request = Request::create($path, 'POST', [], [], [], [], Json::encode($body)); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project completed."', $response->getContent()); + } + + /** + * Tests the project complete resource - manager. + * + * @covers ::access + */ + public function testProjectCompleteManager(): void { + + $project = $this->createProject(ProjectState::ONGOING); + $manager = $project->getOwner()->getManager(); + $participant = $this->createCreative(); + $project->appendParticipant($participant); + $project->save(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project completed."', $response->getContent()); + } + + /** + * Tests the project complete resource - participant. + * + * @covers ::access + */ + public function testProjectCompleteParticipant(): void { + + $project = $this->createProject(ProjectState::ONGOING); + $participant = $this->createCreative(); + $project->appendParticipant($participant); + $project->save(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $participant); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project completed."', $response->getContent()); + } + + /** + * Tests the project complete resource - supervisor. + * + * @covers ::access + */ + public function testProjectCompleteSupervisor(): void { + + $project = $this->createProject(ProjectState::ONGOING); + $supervisor = $this->createSupervisor(); + $participant = $this->createCreative(); + $project->appendParticipant($participant); + $project->save(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $supervisor); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project completed."', $response->getContent()); + } + + /** + * Tests the project complete resource - not ongoing. + * + * @covers ::post + */ + public function testProjectCompleteNotOngoing(): void { + + $project = $this->createProject(ProjectState::OPEN); + $organization = $project->getOwner(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(409, $response->getStatusCode()); + $this->assertEquals('{"message":"Project can not be completed."}', $response->getContent()); + } + + /** + * Tests the project complete resource - not owner. + * + * @covers ::access + */ + public function testProjectCompleteNotOwner(): void { + + $project = $this->createProject(ProjectState::ONGOING); + $other_organization = $this->createOrganization(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $other_organization); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project complete resource - not manager. + * + * @covers ::access + */ + public function testProjectCompleteNotManager(): void { + + $project = $this->createProject(ProjectState::ONGOING); + $other_manager = $this->createManager(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $other_manager); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project complete resource - not participant. + * + * @covers ::access + */ + public function testProjectCompleteNotParticipant(): void { + + $project = $this->createProject(ProjectState::ONGOING); + $other_participant = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $other_participant); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project complete resource - not published (status). + * + * @covers ::access + */ + public function testProjectCompleteNotPublished(): void { + + $project = $this->createProject(); + $project->setUnpublished(); + $project->save(); + $organization = $project->getOwner(); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project complete resource - no permission. + * + * @covers ::access + */ + public function testProjectCompleteNoPermission(): void { + + $project = $this->createProject(); + $editor = $this->createCreative('editor'); + + $path = '/api/projects/' . $project->uuid() . '/complete'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $editor); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The \u0027restful post project:complete\u0027 permission is required."}', $response->getContent()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectMediateResourceTest.php b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectMediateResourceTest.php new file mode 100644 index 00000000..dacbd012 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectMediateResourceTest.php @@ -0,0 +1,217 @@ +createProject(ProjectState::OPEN); + $organization = $project->getOwner(); + $applicant = $this->createCreative(); + $project->appendApplicant($applicant); + $project->save(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $request = Request::create($path, 'GET'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertStringContainsString($project->uuid(), $response->getContent()); + $this->assertStringContainsString($applicant->uuid(), $response->getContent()); + } + + /** + * Tests the project mediate resource - standard POST workflow. + * + * @covers ::post + * @covers ::loadSelectedCreatives + */ + public function testProjectMediate(): void { + + $project = $this->createProject(ProjectState::OPEN); + $organization = $project->getOwner(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $body = [ + 'selected_creatives' => [ + $this->createCreative()->uuid(), + $this->createCreative()->uuid(), + $this->createCreative()->uuid(), + ], + ]; + $request = Request::create($path, 'POST', [], [], [], [], Json::encode($body)); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project mediated."', $response->getContent()); + } + + /** + * Tests the project mediate resource - manager. + * + * @covers ::access + */ + public function testProjectMediateManager(): void { + + $project = $this->createProject(ProjectState::OPEN); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $body = ['selected_creatives' => [$this->createCreative()->uuid()]]; + $request = Request::create($path, 'POST', [], [], [], [], Json::encode($body)); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project mediated."', $response->getContent()); + } + + /** + * Tests the project mediate resource - supervisor. + * + * @covers ::access + */ + public function testProjectMediateSupervisor(): void { + + $project = $this->createProject(ProjectState::OPEN); + $supervisor = $this->createSupervisor(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $body = ['selected_creatives' => [$this->createCreative()->uuid()]]; + $request = Request::create($path, 'POST', [], [], [], [], Json::encode($body)); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $supervisor); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project mediated."', $response->getContent()); + } + + /** + * Tests the project mediate resource - not open. + * + * @covers ::post + */ + public function testProjectMediateNotOpen(): void { + + $project = $this->createProject(ProjectState::COMPLETED); + $organization = $project->getOwner(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $body = ['selected_creatives' => [$this->createCreative()->uuid()]]; + $request = Request::create($path, 'POST', [], [], [], [], Json::encode($body)); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(409, $response->getStatusCode()); + $this->assertEquals('{"message":"Project can not be mediated."}', $response->getContent()); + } + + /** + * Tests the project mediate resource - not owner. + * + * @covers ::access + */ + public function testProjectMediateNotOwner(): void { + + $project = $this->createProject(ProjectState::OPEN); + $other_organization = $this->createOrganization(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $other_organization); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project mediate resource - not manager. + * + * @covers ::access + */ + public function testProjectMediateNotManager(): void { + + $project = $this->createProject(ProjectState::OPEN); + $other_manager = $this->createManager(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $other_manager); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project mediate resource - not published (status). + * + * @covers ::access + */ + public function testProjectMediateNotPublished(): void { + + $project = $this->createProject(); + $project->setUnpublished(); + $project->save(); + $organization = $project->getOwner(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project mediate resource - no permission. + * + * @covers ::access + */ + public function testProjectMediateNoPermission(): void { + + $project = $this->createProject(); + $creative = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/mediate'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The \u0027restful post project:mediate\u0027 permission is required."}', $response->getContent()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectNotifyResourceTest.php b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectNotifyResourceTest.php new file mode 100644 index 00000000..ba03adb0 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectNotifyResourceTest.php @@ -0,0 +1,161 @@ +createProject(); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/notify'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"The organization was notified."', $response->getContent()); + } + + /** + * Tests the for the project notify resource - prospect. + * + * @covers ::post + */ + public function testProjectNotifyProspect(): void { + + $project = $this->createProject(ProjectState::DRAFT, 'prospect'); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/notify'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"The organization was invited."', $response->getContent()); + } + + /** + * Tests the for the project notify resource - supervisor. + * + * @covers ::access + */ + public function testProjectNotifySupervisor(): void { + + $project = $this->createProject(); + $supervisor = $this->createSupervisor(); + + $path = '/api/projects/' . $project->uuid() . '/notify'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $supervisor); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"The organization was notified."', $response->getContent()); + } + + /** + * Tests the for the project notify resource - not draft. + * + * @covers ::access + */ + public function testProjectNotifyNotDraft(): void { + + $project = $this->createProject(ProjectState::OPEN); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/notify'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this action are not met."}', $response->getContent()); + } + + /** + * Tests the for the project notify resource - not manager. + * + * @covers ::access + */ + public function testProjectNotifyNotManager(): void { + + $project = $this->createProject(); + $other_manager = $this->createManager(); + + $path = '/api/projects/' . $project->uuid() . '/notify'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $other_manager); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this action are not met."}', $response->getContent()); + } + + /** + * Tests the for the project notify resource - not published. + * + * @covers ::access + */ + public function testProjectNotifyNotPublished(): void { + + $project = $this->createProject(); + $project->setUnpublished(); + $project->save(); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/notify'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this action are not met."}', $response->getContent()); + } + + /** + * Tests the project notify resource - no permission. + * + * @covers ::access + */ + public function testProjectNotifyNoPermission(): void { + + $project = $this->createProject(); + $creative = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/notify'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The \u0027restful post project:notify\u0027 permission is required."}', $response->getContent()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectPublishResourceTest.php b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectPublishResourceTest.php new file mode 100644 index 00000000..05f6ba5b --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectPublishResourceTest.php @@ -0,0 +1,141 @@ +createProject(ProjectState::PENDING); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/publish'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project published."', $response->getContent()); + } + + /** + * Tests the for the project publish resource - supervisor. + * + * @covers ::access + */ + public function testProjectPublishSupervisor(): void { + + $project = $this->createProject(ProjectState::OPEN); + $supervisor = $this->createSupervisor(); + + $path = '/api/projects/' . $project->uuid() . '/publish'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $supervisor); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project published."', $response->getContent()); + } + + /** + * Tests the for the project publish resource - not pending. + * + * @covers ::post + */ + public function testProjectPublishNotPending(): void { + + $project = $this->createProject(ProjectState::ONGOING); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/publish'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(409, $response->getStatusCode()); + $this->assertEquals('{"message":"Project can not be published."}', $response->getContent()); + } + + /** + * Tests the for the project submit resource - not manager. + * + * @covers ::access + */ + public function testProjectPublishNotManager(): void { + + $project = $this->createProject(); + $other_manager = $this->createManager(); + + $path = '/api/projects/' . $project->uuid() . '/publish'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $other_manager); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the for the project publish resource - not published (status). + * + * @covers ::access + */ + public function testProjectPublishNotPublished(): void { + + $project = $this->createProject(); + $project->setUnpublished(); + $project->save(); + $manager = $project->getOwner()->getManager(); + + $path = '/api/projects/' . $project->uuid() . '/publish'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $manager); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project publish resource - no permission. + * + * @covers ::access + */ + public function testProjectPublishNoPermission(): void { + + $project = $this->createProject(); + $organization = $this->createOrganization(); + + $path = '/api/projects/' . $project->uuid() . '/publish'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The \u0027restful post project:publish\u0027 permission is required."}', $response->getContent()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectResetResourceTest.php b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectResetResourceTest.php new file mode 100644 index 00000000..288f5816 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectResetResourceTest.php @@ -0,0 +1,59 @@ +createProject(ProjectState::OPEN); + $supervisor = $this->createSupervisor(); + + $path = '/api/projects/' . $project->uuid() . '/reset'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $supervisor); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project reset."', $response->getContent()); + } + + /** + * Tests the for the project reset resource - no permission. + * + * @covers ::access + */ + public function testProjectResetNoPermission(): void { + + $project = $this->createProject(); + $creative = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/reset'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The \u0027restful post project:reset\u0027 permission is required."}', $response->getContent()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectResourceTestBase.php b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectResourceTestBase.php new file mode 100644 index 00000000..07c87d73 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectResourceTestBase.php @@ -0,0 +1,182 @@ +container->get('event_dispatcher'); + foreach ($event_dispatcher->getListeners() as $event => $listeners) { + if (str_contains($event, 'projects')) { + foreach ($listeners as $listener) { + if (!str_contains($listener[0]::class, 'projects')) { + $event_dispatcher->removeSubscriber($listener[0]); + } + } + } + } + + // Add the test as an event subscriber for specific test cases. + $event_dispatcher->addSubscriber($this); + } + + /** + * Creates a minimal project for testing purposes. + */ + protected function createProject(ProjectState $state = ProjectState::DRAFT, string $role = 'organization'): ProjectInterface { + + $manager = Creative::create([ + 'name' => $this->randomString(), + 'mail' => 'manager@example.com', + 'pass' => 'password', + 'status' => 1, + ]); + $manager->addRole('creative'); + $manager->addRole('manager'); + $manager->save(); + $this->markEntityForCleanup($manager); + + $organization = Organization::create([ + 'name' => $this->randomString(), + 'mail' => 'test@example.org', + 'field_manager' => $manager, + 'pass' => 'password', + 'status' => 1, + ]); + $organization->addRole($role); + $organization->save(); + $this->markEntityForCleanup($organization); + + $project = Project::create([ + 'type' => 'project', + 'uid' => $organization->id(), + 'status' => ProjectInterface::PUBLISHED, + 'title' => $this->randomString(), + 'field_lifecycle' => $state->value, + ]); + $project->save(); + $this->markEntityForCleanup($project); + + $project_result = $project->getResult(); + $this->markEntityForCleanup($project_result); + + return $project; + } + + /** + * Creates a creative for testing purposes. + */ + protected function createCreative(string $role = 'creative'): Creative { + $creative = Creative::create([ + 'name' => $this->randomString(), + 'mail' => 'test@example.com', + 'pass' => 'password', + 'status' => 1, + ]); + $creative->addRole($role); + $creative->save(); + $this->markEntityForCleanup($creative); + return $creative; + } + + /** + * Creates a manager for testing purposes. + */ + protected function createManager(): Creative { + $manager = Creative::create([ + 'name' => $this->randomString(), + 'mail' => 'other_manager@example.com', + 'pass' => 'password', + 'status' => 1, + ]); + $manager->addRole('creative'); + $manager->addRole('manager'); + $manager->save(); + $this->markEntityForCleanup($manager); + return $manager; + } + + /** + * Creates a creative for testing purposes. + */ + protected function createSupervisor(): Creative { + $supervisor = Creative::create([ + 'name' => $this->randomString(), + 'mail' => 'supervisor@example.com', + 'pass' => 'password', + 'status' => 1, + ]); + $supervisor->addRole('supervisor'); + $supervisor->save(); + $this->markEntityForCleanup($supervisor); + return $supervisor; + } + + /** + * Creates a organization for testing purposes. + */ + protected function createOrganization(): Organization { + $organization = Organization::create([ + 'name' => $this->randomString(), + 'mail' => 'other@example.org', + 'pass' => 'password', + 'status' => 1, + ]); + $organization->addRole('organization'); + $organization->save(); + $this->markEntityForCleanup($organization); + return $organization; + } + + /** + * Listens to the project events to throw an exception for testing purposes. + * + * @throws \LogicException + */ + public function onProjectEvent(Event $event): void { + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + ProjectApplyEvent::class => 'onProjectEvent', + ProjectCompleteEvent::class => 'onProjectEvent', + ProjectMediateEvent::class => 'onProjectEvent', + ProjectPublishEvent::class => 'onProjectEvent', + ProjectResetEvent::class => 'onProjectEvent', + ProjectSubmitEvent::class => 'onProjectEvent', + ]; + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectSubmitResourceTest.php b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectSubmitResourceTest.php new file mode 100644 index 00000000..c34b1a0c --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/ExistingSite/Resource/ProjectSubmitResourceTest.php @@ -0,0 +1,141 @@ +createProject(); + $organization = $project->getOwner(); + + $path = '/api/projects/' . $project->uuid() . '/submit'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project submitted."', $response->getContent()); + } + + /** + * Tests the for the project submit resource - supervisor. + * + * @covers ::access + */ + public function testProjectSubmitSupervisor(): void { + + $project = $this->createProject(); + $supervisor = $this->createSupervisor(); + + $path = '/api/projects/' . $project->uuid() . '/submit'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $supervisor); + + $response = $this->doRequest($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('"Project submitted."', $response->getContent()); + } + + /** + * Tests the for the project submit resource - not draft. + * + * @covers ::post + */ + public function testProjectSubmitNotDraft(): void { + + $project = $this->createProject(ProjectState::OPEN); + $organization = $project->getOwner(); + + $path = '/api/projects/' . $project->uuid() . '/submit'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(409, $response->getStatusCode()); + $this->assertEquals('{"message":"Project can not be submitted."}', $response->getContent()); + } + + /** + * Tests the for the project submit resource - not owner. + * + * @covers ::access + */ + public function testProjectSubmitNotOwner(): void { + + $project = $this->createProject(); + $other_organization = $this->createOrganization(); + + $path = '/api/projects/' . $project->uuid() . '/submit'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $other_organization); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the for the project submit resource - not published. + * + * @covers ::access + */ + public function testProjectSubmitNotPublished(): void { + + $project = $this->createProject(); + $project->setUnpublished(); + $project->save(); + $organization = $project->getOwner(); + + $path = '/api/projects/' . $project->uuid() . '/submit'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $organization); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The project conditions for this transition are not met."}', $response->getContent()); + } + + /** + * Tests the project submit resource - no permission. + * + * @covers ::access + */ + public function testProjectSubmitNoPermission(): void { + + $project = $this->createProject(); + $creative = $this->createCreative(); + + $path = '/api/projects/' . $project->uuid() . '/submit'; + $request = Request::create($path, 'POST'); + $request->headers->set('Content-Type', 'application/json'); + $this->authenticateRequest($request, $creative); + + $response = $this->doRequest($request); + $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals('{"message":"The \u0027restful post project:submit\u0027 permission is required."}', $response->getContent()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectApplyTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectApplyTest.php new file mode 100644 index 00000000..076926f3 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectApplyTest.php @@ -0,0 +1,42 @@ +createProject(ProjectState::OPEN); + $this->assertTrue($project->lifecycle()->isOpen()); + + $creative = $this->createCreative(); + $event = new ProjectApplyEvent($project, $creative); + $event->setPhoneNumber('+1234567890'); + $this->eventDispatcher->dispatch($event); + + $this->assertTrue($project->lifecycle()->isOpen()); + $this->assertTrue($project->hasApplicant()); + $this->assertTrue($project->isApplicant($creative)); + + $applicants = $project->getApplicants(); + $this->assertCount(1, $applicants); + $applicant = reset($applicants); + $this->assertEquals('+1234567890', $applicant->getPhoneNumber()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectCompleteTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectCompleteTest.php new file mode 100644 index 00000000..71c030d3 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectCompleteTest.php @@ -0,0 +1,53 @@ +installConfig('projects_result_test'); + } + + /** + * Tests the project complete event listener. + * + * @covers ::onProjectComplete + * @covers ::getSubscribedEvents + */ + public function testProjectComplete(): void { + + $project = $this->createProject(ProjectState::ONGOING); + $this->assertTrue($project->lifecycle()->isOngoing()); + + $creative = $this->createCreative(); + $project->appendParticipant($creative); + $this->assertTrue($project->hasParticipant('Creative')); + + $event = new ProjectCompleteEvent($project); + $this->eventDispatcher->dispatch($event); + + $this->assertTrue($project->lifecycle()->isCompleted()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectEventSubscriberTestBase.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectEventSubscriberTestBase.php new file mode 100644 index 00000000..3029f473 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectEventSubscriberTestBase.php @@ -0,0 +1,83 @@ +installEntitySchema('project'); + $this->installEntitySchema('project_result'); + $this->installEntitySchema('user'); + $this->installConfig('projects_lifecycle_test'); + $this->eventDispatcher = $this->container->get('event_dispatcher'); + } + + /** + * Creates a minimal project for testing purposes. + */ + protected function createProject(ProjectState $state = ProjectState::DRAFT): ProjectInterface { + + $manager = Creative::create(['name' => $this->randomString()]); + $manager->save(); + + $organization = Organization::create([ + 'name' => $this->randomString(), + 'field_manager' => $manager, + ]); + $organization->save(); + + return Project::create([ + 'type' => 'project', + 'uid' => $organization->id(), + 'status' => ProjectInterface::PUBLISHED, + 'title' => $this->randomString(), + 'field_lifecycle' => $state->value, + ]); + } + + /** + * Creates a creative for testing purposes. + */ + protected function createCreative(): Creative { + $creative = Creative::create(['name' => $this->randomString()]); + $creative->save(); + return $creative; + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectMediateTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectMediateTest.php new file mode 100644 index 00000000..e60831ec --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectMediateTest.php @@ -0,0 +1,40 @@ +createProject(ProjectState::OPEN); + $this->assertTrue($project->lifecycle()->isOpen()); + + $creative = $this->createCreative(); + $project->appendApplicant($creative); + $this->assertTrue($project->hasApplicant()); + + $event = new ProjectMediateEvent($project); + $event->setCreatives([$creative]); + $this->eventDispatcher->dispatch($event); + + $this->assertTrue($project->lifecycle()->isOngoing()); + $this->assertTrue($project->hasParticipant()); + $this->assertTrue($project->isParticipant($creative)); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectPublishTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectPublishTest.php new file mode 100644 index 00000000..e92516b1 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectPublishTest.php @@ -0,0 +1,30 @@ +createProject(ProjectState::PENDING); + $this->assertTrue($project->lifecycle()->isPending()); + $event = new ProjectPublishEvent($project); + $this->eventDispatcher->dispatch($event); + $this->assertTrue($project->lifecycle()->isOpen()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectResetTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectResetTest.php new file mode 100644 index 00000000..8ad33c89 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectResetTest.php @@ -0,0 +1,31 @@ +createProject(ProjectState::OPEN); + $this->assertTrue($project->lifecycle()->isOpen()); + $event = new ProjectResetEvent($project); + $this->eventDispatcher->dispatch($event); + $this->assertTrue($project->lifecycle()->isDraft()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectSubmitTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectSubmitTest.php new file mode 100644 index 00000000..c31b5102 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectSubmitTest.php @@ -0,0 +1,29 @@ +createProject(); + $this->assertTrue($project->lifecycle()->isDraft()); + $event = new ProjectSubmitEvent($project); + $this->eventDispatcher->dispatch($event); + $this->assertTrue($project->lifecycle()->isPending()); + } + +} diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/ProjectTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/ProjectTest.php deleted file mode 100644 index 397f86c2..00000000 --- a/web/modules/custom/projects/projects/tests/src/Kernel/ProjectTest.php +++ /dev/null @@ -1,202 +0,0 @@ -setParameter('jsonapi_obscurity.prefix', '/12345'); - $container->setParameter('jsonapi.base_path', '/api'); - } - - /** - * {@inheritdoc} - * - * @throws \Drupal\Core\Entity\EntityStorageException - */ - public function setUp(): void { - parent::setUp(); - - $this->installConfig(['language']); - ConfigurableLanguage::createFromLangcode('de')->save(); - - $this->installEntitySchema('user'); - $this->installEntitySchema('path_alias'); - $this->installEntitySchema('taxonomy_term'); - $this->installConfig('user_bundle'); - $this->installConfig('user_types'); - $this->installConfig('creatives'); - $this->installConfig('organizations'); - $this->installConfig('filter'); - - $this->installConfig('youvo'); - $this->installEntitySchema('project'); - $this->installEntitySchema('project_result'); - $this->installConfig('projects'); - - $this->admin = $this->createUser( - ['administer projects'], - 'Admira Minelli', - TRUE, - ['uid' => 1], - ); - $this->admin->addRole('administrator'); - $this->admin->save(); - $this->createOrganization(); - - // Create node with random information. - $project = Project::create([ - 'uid' => 3, - 'type' => 'project', - 'title' => 'Test Project', - 'body' => [ - 'value' => 'Test', - 'summary' => 'Test', - ], - 'langcode' => 'de', - 'status' => 1, - 'field_allowance' => 'Test', - 'field_contact' => 'Test', - 'field_workload' => 1, - 'field_city' => 'Test', - 'field_material' => 'Test', - 'field_appreciation' => 'Test', - 'field_image_copyright' => 'Test', - 'field_lifecycle' => 'draft', - ]); - - $project->save(); - } - - /** - * Tests get request for projects collection. - */ - public function testDoRequest(): void { - - $this->setCurrentUser($this->admin); - $request = Request::create('/12345/api/projects'); - $this->doRequest($request); - self::assertStringContainsString( - 'Test Project', - $this->getRawContent() - ); - } - - /** - * Creates an organization. - * - * @throws \Drupal\Core\Entity\EntityStorageException - */ - protected function createOrganization(): void { - - // Object for random content. - $faker = Factory::create('de_DE'); - $faker->seed(96886); - - $values = [ - 'field_name' => 'Test Organization e.V.', - 'field_short_name' => 'Organization', - 'field_url' => 'https://www.' . $faker->domainName(), - 'field_aim' => $faker->text(80), - 'field_about' => $faker->text(600), - 'field_count_volunteer' => $faker->numberBetween(5, 20), - 'field_count_fulltime' => $faker->numberBetween(1, 5), - 'field_contact' => $faker->lastName(), - 'field_phone' => $faker->phoneNumber(), - 'field_reachability' => $faker->text(80), - 'field_referral' => $faker->text(), - 'field_street' => $faker->streetAddress(), - 'field_zip' => $faker->postcode(), - 'field_city' => $faker->city(), - 'field_country' => $faker->optional(0.2)->country(), - 'field_budget' => $faker->text(), - 'field_publicity' => $faker->optional()->text(), - 'name' => 'test@example.com', - 'mail' => 'test@example.com', - 'type' => 'organization', - 'pass' => 'password', - 'status' => 1, - 'uid' => 3, - ]; - - // Create organization. - $organization = Organization::create($values); - $organization->addRole('organization'); - $organization->save(); - } - -} diff --git a/web/modules/custom/projects/projects/tests/src/Unit/ProjectLifecycleTest.php b/web/modules/custom/projects/projects/tests/src/Unit/ProjectLifecycleTest.php index 8091cdd9..7557d342 100644 --- a/web/modules/custom/projects/projects/tests/src/Unit/ProjectLifecycleTest.php +++ b/web/modules/custom/projects/projects/tests/src/Unit/ProjectLifecycleTest.php @@ -129,9 +129,9 @@ protected function prophesizeProject(ProjectState $state): void { * The transition. * @param array $allowed_from * An array to mock the workflow allowed from settings. - * @param array $has_transition_return_values + * @param array $has_transition * An array to mock the workflow transition settings. - * @param bool $has_applicant + * @param bool $has_participant * Whether the project has an applicant. Relevant for mediate transition. * * @covers ::submit @@ -146,14 +146,15 @@ protected function prophesizeProject(ProjectState $state): void { * * @dataProvider doTransitionProvider */ - public function testDoTransition(ProjectTransition $transition, array $allowed_from, array $has_transition_return_values, bool $has_applicant): void { - $this->prophesizeWorkflow($has_transition_return_values, $has_applicant); - foreach (ProjectState::cases() as $state) { + public function testDoTransition(ProjectTransition $transition, array $allowed_from, array $has_transition, bool $has_participant): void { + $states = ProjectState::cases(); + $this->prophesizeWorkflow($states, $has_transition, $has_participant); + foreach ($states as $state) { try { $this->assertTrue($this->lifecycle->{$transition->value}()); } catch (LifecycleTransitionException) { - $this->assertFalse(in_array($state, $allowed_from, TRUE) && $has_applicant); + $this->assertFalse(in_array($state, $allowed_from, TRUE) && $has_participant); } } } @@ -166,47 +167,46 @@ public static function doTransitionProvider(): array { $cases[ProjectTransition::SUBMIT->value] = [ 'transition' => ProjectTransition::SUBMIT, 'allowed_from' => [ProjectState::DRAFT], - 'has_transition_return_values' => [TRUE, FALSE, FALSE, FALSE, FALSE], + 'has_transition' => [TRUE, FALSE, FALSE, FALSE, FALSE], // Not relevant for this case. - 'has_applicant' => FALSE, + 'has_participant' => FALSE, ]; $cases[ProjectTransition::PUBLISH->value] = [ 'transition' => ProjectTransition::PUBLISH, 'allowed_from' => [ProjectState::PENDING], - 'has_transition_return_values' => [FALSE, TRUE, FALSE, FALSE, FALSE], + 'has_transition' => [FALSE, TRUE, FALSE, FALSE, FALSE], // Not relevant for this case. - 'has_applicant' => FALSE, + 'has_participant' => FALSE, ]; - $cases[ProjectTransition::MEDIATE->value . '-without-applicant'] = [ + $cases[ProjectTransition::MEDIATE->value . '-without-participant'] = [ 'transition' => ProjectTransition::MEDIATE, 'allowed_from' => [ProjectState::OPEN], - 'has_transition_return_values' => [FALSE, FALSE, FALSE, FALSE, FALSE], - 'has_applicant' => FALSE, + 'has_transition' => [FALSE, FALSE, FALSE, FALSE, FALSE], + 'has_participant' => FALSE, ]; - $cases[ProjectTransition::MEDIATE->value . '-with-applicant'] = [ + $cases[ProjectTransition::MEDIATE->value . '-with-participant'] = [ 'transition' => ProjectTransition::MEDIATE, 'allowed_from' => [ProjectState::OPEN], - 'has_transition_return_values' => [FALSE, FALSE, TRUE, FALSE, FALSE], - 'has_applicant' => TRUE, + 'has_transition' => [FALSE, FALSE, TRUE, FALSE, FALSE], + 'has_participant' => TRUE, ]; $cases[ProjectTransition::COMPLETE->value] = [ 'transition' => ProjectTransition::COMPLETE, 'allowed_from' => [ProjectState::ONGOING], - 'has_transition_return_values' => [FALSE, FALSE, FALSE, TRUE, FALSE], - // Not relevant for this case. - 'has_applicant' => FALSE, + 'has_transition' => [FALSE, FALSE, FALSE, TRUE, FALSE], + 'has_participant' => TRUE, ]; $cases[ProjectTransition::RESET->value] = [ 'transition' => ProjectTransition::RESET, 'allowed_from' => ProjectState::cases(), - 'has_transition_return_values' => [TRUE, TRUE, TRUE, TRUE, TRUE], + 'has_transition' => [TRUE, TRUE, TRUE, TRUE, TRUE], // Not relevant for this case. - 'has_applicant' => FALSE, + 'has_participant' => FALSE, ]; return $cases; @@ -215,10 +215,11 @@ public static function doTransitionProvider(): array { /** * Prophesizes a project lifecycle with different workflow conditions. */ - protected function prophesizeWorkflow(array $has_transition_return_values, bool $has_applicant): void { + protected function prophesizeWorkflow(array $states, array $has_transition, bool $has_participant): void { $lifecycle_config = $this->prophesize(Lifecycle::class); - $lifecycle_config->hasTransition(Argument::any())->willReturn(...$has_transition_return_values); + $lifecycle_config->hasTransitionFromStateToState(Argument::any(), Argument::any()) + ->willReturn(...$has_transition); $workflow = $this->prophesize(Workflow::class); $workflow->getTypePlugin()->willReturn($lifecycle_config->reveal()); @@ -229,9 +230,11 @@ protected function prophesizeWorkflow(array $has_transition_return_values, bool $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); $entity_type_manager->getStorage('workflow')->willReturn($workflow_storage->reveal()); + $values = array_map(static fn($s) => (object) ['value' => $s->value], $states); $project = $this->prophesize(ProjectInterface::class); + $project->get(Argument::is(ProjectLifecycle::LIFECYCLE_FIELD))->willReturn(...$values); $project->set(Argument::any(), Argument::any())->willReturn(TRUE); - $project->hasApplicant()->willReturn($has_applicant); + $project->hasParticipant(Argument::is('Creative'))->willReturn($has_participant); $this->lifecycle = new ProjectLifecycle($entity_type_manager->reveal()); $this->lifecycle->setProject($project->reveal()); diff --git a/web/modules/custom/user_types/organizations/src/Entity/Organization.php b/web/modules/custom/user_types/organizations/src/Entity/Organization.php index 930794db..e2aaa56a 100644 --- a/web/modules/custom/user_types/organizations/src/Entity/Organization.php +++ b/web/modules/custom/user_types/organizations/src/Entity/Organization.php @@ -62,10 +62,12 @@ public function hasManager(): bool { public function getManager(): ?Creative { /** @var \Drupal\Core\Field\EntityReferenceFieldItemList $manager_field */ $manager_field = $this->get('field_manager'); - $manager_references = $manager_field->referencedEntities(); - /** @var \Drupal\creatives\Entity\Creative|null $manager */ - $manager = reset($manager_references); - return $manager ?: NULL; + if ($managers = $manager_field->referencedEntities()) { + /** @var \Drupal\creatives\Entity\Creative|null $manager */ + $manager = reset($managers); + return $manager; + } + return NULL; } /** diff --git a/web/modules/custom/user_types/tests/src/Unit/ProfileUtilityTest.php b/web/modules/custom/user_types/tests/src/Unit/ProfileUtilityTest.php index 84251a09..205d53e1 100644 --- a/web/modules/custom/user_types/tests/src/Unit/ProfileUtilityTest.php +++ b/web/modules/custom/user_types/tests/src/Unit/ProfileUtilityTest.php @@ -17,57 +17,43 @@ /** * Test coverage for the profile utility class. * + * @coversDefaultClass \Drupal\user_types\Utility\Profile * @group user_types */ class ProfileUtilityTest extends UnitTestCase { /** - * The account mock user. - * - * @var \Drupal\Core\Session\AccountInterface + * The mock account. */ protected AccountInterface $account; /** - * The account proxy mock user. - * - * @var \Drupal\Core\Session\AccountProxyInterface + * The mock account proxy. */ protected AccountProxyInterface $accountProxy; /** * The mock creative. - * - * @var \Drupal\creatives\Entity\Creative */ protected Creative $creative; /** - * The mock creative. - * - * @var \Drupal\organizations\Entity\Organization + * The mock organization. */ protected Organization $organization; /** * The mock creative auth user. - * - * @var \Drupal\simple_oauth\Authentication\TokenAuthUserInterface */ protected TokenAuthUserInterface $creativeAuthUser; /** * The mock organization auth user. - * - * @var \Drupal\simple_oauth\Authentication\TokenAuthUserInterface */ protected TokenAuthUserInterface $organizationAuthUser; /** * {@inheritdoc} - * - * @throws \League\OAuth2\Server\Exception\OAuthServerException - * When the user can not be identified. */ protected function setUp(): void { @@ -78,11 +64,6 @@ protected function setUp(): void { ->method('id') ->willReturn(1); - $this->accountProxy = $this->createMock(AccountProxyInterface::class); - $this->accountProxy->expects($this->any()) - ->method('id') - ->willReturn(2); - $this->creative = $this->createMock(Creative::class); $this->creative->expects($this->any()) ->method('id') @@ -93,6 +74,15 @@ protected function setUp(): void { ->method('bundle') ->willReturn('user'); + $this->accountProxy = $this->createMock(AccountProxyInterface::class); + $this->accountProxy->expects($this->any()) + ->method('id') + ->willReturn(2); + // For testing, we will proxy the creative. + $this->accountProxy->expects($this->any()) + ->method('getAccount') + ->willReturn($this->creative); + $this->organization = $this->createMock(Organization::class); $this->organization->expects($this->any()) ->method('id') @@ -132,7 +122,9 @@ protected function setUp(): void { } /** - * @covers \Drupal\user_types\Utility\Profile::id + * Tests the id method. + * + * @covers ::id */ public function testId(): void { $this->assertSame(1, Profile::id($this->account)); @@ -146,9 +138,13 @@ public function testId(): void { } /** - * @covers \Drupal\user_types\Utility\Profile::isCreative + * Tests the isCreative method. + * + * @covers ::isCreative + * @covers ::isUserType */ public function testIsCreative(): void { + $this->assertTrue(Profile::isCreative($this->accountProxy)); $this->assertTrue(Profile::isCreative($this->creative)); $this->assertTrue(Profile::isCreative($this->creativeAuthUser)); $this->assertFalse(Profile::isCreative($this->organization)); @@ -156,9 +152,13 @@ public function testIsCreative(): void { } /** - * @covers \Drupal\user_types\Utility\Profile::isOrganization + * Tests the isOrganization method. + * + * @covers ::isOrganization + * @covers ::isUserType */ public function testIsOrganization(): void { + $this->assertFalse(Profile::isOrganization($this->accountProxy)); $this->assertFalse(Profile::isOrganization($this->creative)); $this->assertFalse(Profile::isOrganization($this->creativeAuthUser)); $this->assertTrue(Profile::isOrganization($this->organization)); diff --git a/web/modules/custom/youvo/src/Test/RequestTrait.php b/web/modules/custom/youvo/tests/src/Traits/RequestTrait.php similarity index 67% rename from web/modules/custom/youvo/src/Test/RequestTrait.php rename to web/modules/custom/youvo/tests/src/Traits/RequestTrait.php index 7a526d65..2550e859 100644 --- a/web/modules/custom/youvo/src/Test/RequestTrait.php +++ b/web/modules/custom/youvo/tests/src/Traits/RequestTrait.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace Drupal\youvo\Test; +namespace Drupal\Tests\youvo\Traits; +use Drupal\Core\Session\AccountInterface; use Drupal\KernelTests\AssertContentTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -19,20 +20,28 @@ trait RequestTrait { /** * Passes a request to the HTTP kernel and returns a response. * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request. - * - * @return \Symfony\Component\HttpFoundation\Response - * The response. + * @throws \Exception */ protected function doRequest(Request $request): Response { + $http_kernel = $this->container->get('http_kernel'); self::assertInstanceOf(HttpKernelInterface::class, $http_kernel); + $response = $http_kernel->handle($request); + $content = $response->getContent(); self::assertNotFalse($content); $this->setRawContent($content); + return $response; } + /** + * Authenticates a request with basic auth. + */ + protected function authenticateRequest(Request $request, AccountInterface $account): void { + $request->headers->set('PHP_AUTH_USER', $account->getEmail()); + $request->headers->set('PHP_AUTH_PW', 'password'); + } + } diff --git a/web/modules/custom/youvo/youvo.module b/web/modules/custom/youvo/youvo.module index 36a5c85e..8fea0e6f 100644 --- a/web/modules/custom/youvo/youvo.module +++ b/web/modules/custom/youvo/youvo.module @@ -74,7 +74,9 @@ function youvo_formats_remove_help(array $form_element, FormStateInterface $form * Implements hook_entity_type_alter(). */ function youvo_entity_type_alter(array &$entity_types): void { - $entity_types['taxonomy_term']->setHandlerClass('translation', AcademyTranslationHandler::class); + if (\Drupal::moduleHandler()->moduleExists('taxonomy')) { + $entity_types['taxonomy_term']->setHandlerClass('translation', AcademyTranslationHandler::class); + } } /**