diff --git a/.git_hooks/external_runtime b/.git_hooks/external_runtime deleted file mode 100755 index 7569e89..0000000 --- a/.git_hooks/external_runtime +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -############################################################################### -# External Runtime - вспомогательный скрипт для запуска хуков в отдельной # -# среде, например в контейнере. # -# Использование: # -# 1. Подключите этот файл в скрипт хука: # -# source . $(pwd)/.git_hooks/external_runtime # -# 2. Создайте выше по файловой системе файл .external-runtime, # -# содержащий переменную EXTERNAL_RUNTIME_COMMAND, например: # -# EXTERNAL_RUNTIME_COMMAND="ensi exec-script" -# Этот файл должен отсутствовать в среде запуска. -############################################################################### - -EXTERNAL_RUNTIME_FILE=".external-runtime" - -ESC_SEQ="\x1b[" -COL_RESET=$ESC_SEQ"39;49;00m" -COL_BLUE=$ESC_SEQ"0;34m" - -findUp () { - path=$(pwd) - while [[ "$path" != "" && ! -e "$path/$1" ]]; do - path=${path%/*} - done - echo "$path" -} - -# search external runtime file -rcpath=$(findUp "$EXTERNAL_RUNTIME_FILE") -if [[ "$rcpath" != "" ]]; then - # source it and run hook in via runtime command - . $rcpath/$EXTERNAL_RUNTIME_FILE - printf "$COL_BLUE%s$COL_RESET\n" "External runtime has been found. Running hook with '$EXTERNAL_RUNTIME_COMMAND' command" - $EXTERNAL_RUNTIME_COMMAND $0 - exit $? -fi \ No newline at end of file diff --git a/.git_hooks/post-checkout/01-install-dependencies.sh b/.git_hooks/post-checkout/01-install-dependencies.sh new file mode 120000 index 0000000..43a65ea --- /dev/null +++ b/.git_hooks/post-checkout/01-install-dependencies.sh @@ -0,0 +1 @@ +../scripts/install-dependencies.sh \ No newline at end of file diff --git a/.git_hooks/post-checkout/clear-cache.sh b/.git_hooks/post-checkout/clear-cache.sh deleted file mode 100755 index efd3202..0000000 --- a/.git_hooks/post-checkout/clear-cache.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# В данном хуке выполняется сброс кэша при переключении веток - -source $(pwd)/.git_hooks/external_runtime - -ESC_SEQ="\x1b[" -COL_RESET=$ESC_SEQ"39;49;00m" -COL_RED=$ESC_SEQ"0;31m" -COL_GREEN=$ESC_SEQ"0;32m" -COL_YELLOW=$ESC_SEQ"0;33m" - -php artisan optimize:clear -exit 0 diff --git a/.git_hooks/post-checkout/install-dependecies.sh b/.git_hooks/post-checkout/install-dependecies.sh deleted file mode 100755 index 5582c7d..0000000 --- a/.git_hooks/post-checkout/install-dependecies.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# В данном хуке выполняется -# - composer install если был изменен composer.lock -# - npm install если был изменен package-lock.json - -source $(pwd)/.git_hooks/external_runtime - -ESC_SEQ="\x1b[" -COL_RESET=$ESC_SEQ"39;49;00m" -COL_RED=$ESC_SEQ"0;31m" -COL_GREEN=$ESC_SEQ"0;32m" -COL_YELLOW=$ESC_SEQ"0;33m" - -changed_files="$(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD)" - -check_run() { - echo "$changed_files" | grep -q "$1" && echo " * changes detected in $1" && echo " * running $2" && eval "$2" -} - -check_run composer.lock "composer install" -check_run package-lock.json "npm install" -exit 0 diff --git a/.git_hooks/post-merge/01-install-dependencies.sh b/.git_hooks/post-merge/01-install-dependencies.sh new file mode 120000 index 0000000..43a65ea --- /dev/null +++ b/.git_hooks/post-merge/01-install-dependencies.sh @@ -0,0 +1 @@ +../scripts/install-dependencies.sh \ No newline at end of file diff --git a/.git_hooks/post-merge/clear-cache.sh b/.git_hooks/post-merge/clear-cache.sh deleted file mode 100755 index 757988d..0000000 --- a/.git_hooks/post-merge/clear-cache.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# В данном хуке выполняется сброс кэша при пулле/мерже - -source $(pwd)/.git_hooks/external_runtime - -ESC_SEQ="\x1b[" -COL_RESET=$ESC_SEQ"39;49;00m" -COL_RED=$ESC_SEQ"0;31m" -COL_GREEN=$ESC_SEQ"0;32m" -COL_YELLOW=$ESC_SEQ"0;33m" - -php artisan optimize:clear -exit 0 diff --git a/.git_hooks/pre-commit/01-lint-php.sh b/.git_hooks/pre-commit/01-lint-php.sh new file mode 120000 index 0000000..5833040 --- /dev/null +++ b/.git_hooks/pre-commit/01-lint-php.sh @@ -0,0 +1 @@ +../scripts/lint-php.sh \ No newline at end of file diff --git a/.git_hooks/pre-commit/02-php-cs-fixer.sh b/.git_hooks/pre-commit/02-php-cs-fixer.sh new file mode 120000 index 0000000..66c2032 --- /dev/null +++ b/.git_hooks/pre-commit/02-php-cs-fixer.sh @@ -0,0 +1 @@ +../scripts/php-cs-fixer.sh \ No newline at end of file diff --git a/.git_hooks/pre-commit/lint-openapi.sh b/.git_hooks/pre-commit/lint-openapi.sh deleted file mode 100755 index 376516f..0000000 --- a/.git_hooks/pre-commit/lint-openapi.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -# В данном хуке выполняется приведение код-стайла в соответствие с конфигом .php_cs - -source $(pwd)/.git_hooks/external_runtime - -EXECUTABLE_NAME=spectral -ROOT=`pwd` -ESC_SEQ="\x1b[" -COL_RESET=$ESC_SEQ"39;49;00m" -COL_RED=$ESC_SEQ"0;31m" -COL_GREEN=$ESC_SEQ"0;32m" -COL_YELLOW=$ESC_SEQ"0;33m" -COL_BLUE=$ESC_SEQ"0;34m" -COL_MAGENTA=$ESC_SEQ"0;35m" -COL_CYAN=$ESC_SEQ"0;36m" - -echo "" -printf "$COL_YELLOW%s$COL_RESET\n" "Running pre-commit hook: \"lint-openapi\"" - -# possible locations -locations=( - $ROOT/node_modules/.bin/$EXECUTABLE_NAME - $EXECUTABLE_NAME -) - -for location in ${locations[*]} -do - if [[ -x $location ]]; then - EXECUTABLE=$location - break - fi -done - -if [[ ! -x $EXECUTABLE ]]; then - echo "executable $EXECUTABLE_NAME not found, exiting..." - echo "if you're sure this is incorrect, make sure they're executable (chmod +x)" - exit -fi - -echo "using \"$EXECUTABLE_NAME\" located at $EXECUTABLE" - -if [[ -f $ROOT/$CONFIG_FILE ]]; then - CONFIG=$ROOT/$CONFIG_FILE - echo "config file located at $CONFIG loaded" -fi - -FILES=`git status --porcelain | grep -e '^[AM]\(.*\).yaml$' | cut -c 3-` -if [ -z "$FILES" ]; then - echo "No yaml files changed" -else - echo "Linting openapi files according to .spectral.yaml"; - $EXECUTABLE lint ./public/api-docs/**/index.yaml; - if [ $? == 1 ]; then - printf "$COL_RED%s$COL_RESET\r\n\r\n" "Please fix errors above" - exit 1 -fi -fi - - - -echo "Okay" -exit 0 diff --git a/.git_hooks/pre-push/01-composer-validate.sh b/.git_hooks/pre-push/01-composer-validate.sh new file mode 120000 index 0000000..61e981f --- /dev/null +++ b/.git_hooks/pre-push/01-composer-validate.sh @@ -0,0 +1 @@ +../scripts/composer-validate.sh \ No newline at end of file diff --git a/.git_hooks/pre-push/02-phpstan.sh b/.git_hooks/pre-push/02-phpstan.sh new file mode 120000 index 0000000..05c8a9b --- /dev/null +++ b/.git_hooks/pre-push/02-phpstan.sh @@ -0,0 +1 @@ +../scripts/phpstan.sh \ No newline at end of file diff --git a/.git_hooks/pre-push/03-test-code.sh b/.git_hooks/pre-push/03-test-code.sh new file mode 120000 index 0000000..b33ab89 --- /dev/null +++ b/.git_hooks/pre-push/03-test-code.sh @@ -0,0 +1 @@ +../scripts/test-code.sh \ No newline at end of file diff --git a/.git_hooks/pre-push/var-dump-checker.sh b/.git_hooks/pre-push/var-dump-checker.sh deleted file mode 100755 index 585f820..0000000 --- a/.git_hooks/pre-push/var-dump-checker.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -# В данном хуке выполняется проверка файлов проекта на dd, var_dump и прочие подобные отладочные функции через var-dump-check - -source $(pwd)/.git_hooks/external_runtime - -ESC_SEQ="\x1b[" -COL_RESET=$ESC_SEQ"39;49;00m" -COL_RED=$ESC_SEQ"0;31m" -COL_GREEN=$ESC_SEQ"0;32m" -COL_YELLOW=$ESC_SEQ"0;33m" - -echo -printf "$COL_YELLOW%s$COL_RESET\n" "Running pre-push hook: \"var-dump-checker\"" - -./vendor/bin/var-dump-check --laravel --exclude bootstrap --exclude node_modules --exclude clients --exclude vendor . - -# If the grep command has no hits - echo a warning and exit with non-zero status. -if [ $? == 1 ]; then - printf "$COL_RED%s$COL_RESET\r\n\r\n" "Some var_dump usage found. Please fix your code" - exit 1 -fi - -echo "Okay" -exit 0 diff --git a/.git_hooks/pre-push/composer-validate.sh b/.git_hooks/scripts/composer-validate.sh similarity index 74% rename from .git_hooks/pre-push/composer-validate.sh rename to .git_hooks/scripts/composer-validate.sh index 5d03e15..543451a 100755 --- a/.git_hooks/pre-push/composer-validate.sh +++ b/.git_hooks/scripts/composer-validate.sh @@ -1,8 +1,6 @@ #!/bin/bash -# В данном хуке выполняется валидация composer.json перед коммитом - -source $(pwd)/.git_hooks/external_runtime +# Validate composer.json before commit ESC_SEQ="\x1b[" COL_RESET=$ESC_SEQ"39;49;00m" diff --git a/.git_hooks/post-merge/install-dependecies.sh b/.git_hooks/scripts/install-dependencies.sh similarity index 53% rename from .git_hooks/post-merge/install-dependecies.sh rename to .git_hooks/scripts/install-dependencies.sh index 5582c7d..3b308e5 100755 --- a/.git_hooks/post-merge/install-dependecies.sh +++ b/.git_hooks/scripts/install-dependencies.sh @@ -1,10 +1,6 @@ #!/bin/bash -# В данном хуке выполняется -# - composer install если был изменен composer.lock -# - npm install если был изменен package-lock.json - -source $(pwd)/.git_hooks/external_runtime +# - 'composer update' if changed composer.json ESC_SEQ="\x1b[" COL_RESET=$ESC_SEQ"39;49;00m" @@ -18,6 +14,5 @@ check_run() { echo "$changed_files" | grep -q "$1" && echo " * changes detected in $1" && echo " * running $2" && eval "$2" } -check_run composer.lock "composer install" -check_run package-lock.json "npm install" +check_run composer.json "composer update" exit 0 diff --git a/.git_hooks/pre-commit/lint-php.sh b/.git_hooks/scripts/lint-php.sh similarity index 86% rename from .git_hooks/pre-commit/lint-php.sh rename to .git_hooks/scripts/lint-php.sh index ba4a2c3..2850447 100755 --- a/.git_hooks/pre-commit/lint-php.sh +++ b/.git_hooks/scripts/lint-php.sh @@ -1,8 +1,6 @@ #!/bin/bash -# В данном хуке выполняется линтинг всех добавленных в коммит php файлов через php -l - -. $(pwd)/.git_hooks/external_runtime +# Lint all added php-files via 'php -l' ROOT_DIR="$(pwd)/" LIST=$(git diff-index --cached --name-only --diff-filter=ACMR HEAD) diff --git a/.git_hooks/pre-commit/php-cs-fixer.sh b/.git_hooks/scripts/php-cs-fixer.sh similarity index 88% rename from .git_hooks/pre-commit/php-cs-fixer.sh rename to .git_hooks/scripts/php-cs-fixer.sh index 45fb666..3fe095a 100755 --- a/.git_hooks/pre-commit/php-cs-fixer.sh +++ b/.git_hooks/scripts/php-cs-fixer.sh @@ -1,8 +1,6 @@ #!/bin/bash -# В данном хуке выполняется приведение код-стайла в соответствие с конфигом .php-cs-fixer.php - -source $(pwd)/.git_hooks/external_runtime +# Check code style via '.php-cs-fixer.php' EXECUTABLE_NAME=php-cs-fixer EXECUTABLE_COMMAND=fix diff --git a/.git_hooks/scripts/phpstan.sh b/.git_hooks/scripts/phpstan.sh new file mode 100755 index 0000000..7c15b55 --- /dev/null +++ b/.git_hooks/scripts/phpstan.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Run composer phpstan + +ESC_SEQ="\x1b[" +COL_RESET=$ESC_SEQ"39;49;00m" +COL_RED=$ESC_SEQ"0;31m" +COL_GREEN=$ESC_SEQ"0;32m" +COL_YELLOW=$ESC_SEQ"0;33m" + +echo +printf "$COL_YELLOW%s$COL_RESET\n" "Running pre-push hook: \"phpstan\"" + +if composer phpstan; then + echo "Okay" + exit 0 +else + printf "$COL_RED%s$COL_RESET\r\n" "phpstan analysis failed." + exit 1 +fi diff --git a/.git_hooks/pre-push/test-code.sh b/.git_hooks/scripts/test-code.sh similarity index 89% rename from .git_hooks/pre-push/test-code.sh rename to .git_hooks/scripts/test-code.sh index ac102a3..faa22a1 100755 --- a/.git_hooks/pre-push/test-code.sh +++ b/.git_hooks/scripts/test-code.sh @@ -1,6 +1,6 @@ #!/bin/bash -source $(pwd)/.git_hooks/external_runtime +# Run composer test ESC_SEQ="\x1b[" COL_RESET=$ESC_SEQ"39;49;00m" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..8169284 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..79c6717 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +If you discover any security related issues, please email mail@greensight.ru instead of using the issue tracker. \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1e6ee26..8cc5b7e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -12,22 +12,9 @@ jobs: strategy: fail-fast: true matrix: - php: [8.0, 8.1, 8.2, 8.3] - laravel: [8.*, 9.*, 10.*, 11.*] - include: - - laravel: 8.* - testbench: ^6.23 - - laravel: 9.* - testbench: 7.* - - laravel: 10.* - testbench: 8.* - - laravel: 11.* - testbench: 9.* + php: [8.1, 8.2, 8.3] + laravel: [9.*, 10.*, 11.*] exclude: - - laravel: 10.* - php: 8.0 - - laravel: 11.* - php: 8.0 - laravel: 11.* php: 8.1 @@ -51,7 +38,14 @@ jobs: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update composer update --prefer-stable --prefer-dist --no-interaction + + - name: Composer Validate + run: ./.git_hooks/scripts/composer-validate.sh + - name: Execute tests - run: vendor/bin/phpunit + run: composer test-ci + + - name: Execute phpstan + run: ./.git_hooks/scripts/phpstan.sh diff --git a/.gitignore b/.gitignore index 8c10629..daa725d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,85 @@ +# Project # +######################## +.php_cs.cache +.php-cs-fixer.cache +.huskyrc +clients/* +!clients/.gitkeep +storage/ensi +generated +studio.json +build +/node_modules +/vendor +.phpunit.result.cache +composer.lock +composer.local.json +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log + # IDEs # ################### *.sublime-project *.sublime-workspace -/.idea/ +/.idea +/.vscode *.komodoproject +.vscode -vendor -node_modules/ -.php_cs.cache -.php-cs-fixer.cache -.huskyrc -composer.lock -npm-debug.log -package-lock.json -.phpunit.result.cache +# Static content # +################### +*.csv +*.pdf +*.doc +*.docx +*.xls +*.xlsx +*.xml +!phpunit.xml +!psalm.xml +*.yml +*.txt +*.wav +*.mp3 +*.avi + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.box + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.tgz +*.iso +*.jar +*.rar +*.tar +*.zip +*.phar + +# OS generated files # +###################### +.DS_Store +.DS_Store? +.nfs* +._* +.Spotlight-V100 +.Trashes +.vagrant +ehthumbs.db +Thumbs.db +sftp-config.json +auth.json \ No newline at end of file diff --git a/.huskyrc.json b/.huskyrc.json deleted file mode 100644 index 28f9d0e..0000000 --- a/.huskyrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "hooks": { - "pre-commit": ".git_hooks/pre-commit/lint-php.sh && .git_hooks/pre-commit/php-cs-fixer.sh", - "pre-push": ".git_hooks/pre-push/composer-validate.sh && .git_hooks/pre-push/var-dump-checker.sh && .git_hooks/pre-push/test-code.sh" - } - } \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 35cedfd..a2dce95 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,6 +1,6 @@ in([ __DIR__ . '/src', __DIR__ . '/tests', @@ -13,12 +13,15 @@ return (new PhpCsFixer\Config()) ->setRules([ '@PSR2' => true, + '@PSR12' => true, 'array_syntax' => ['syntax' => 'short'], 'ordered_imports' => ['sort_algorithm' => 'alpha'], 'no_unused_imports' => true, 'trailing_comma_in_multiline' => true, 'phpdoc_scalar' => true, 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'concat_space' => ['spacing' => 'one'], 'blank_line_before_statement' => [ 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], ], @@ -34,5 +37,8 @@ 'keep_multiple_spaces_after_comma' => true, ], 'single_trait_insert_per_statement' => true, + 'no_whitespace_in_blank_line' => true, + 'method_chaining_indentation' => true, + 'single_space_around_construct' => true, ]) ->setFinder($finder); diff --git a/LICENSE.md b/LICENSE.md index 074a245..2ed94d2 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,8 +1,75 @@ -The MIT License (MIT) -Copyright (C) 2015-2018 Antério Vieira, Quetzy Garcia, Raphael França, 2021 Ensi mail@greensight.ru +Открытая лицензия на право использования программы для ЭВМ Greensight Ecom Platform (GEP) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +1. Преамбула -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +1.1. Общество с ограниченной ответственностью «ГринСайт», в лице генерального директора Волкова Егора Владимировича, действующего на основании Устава, публикует условия публичной оферты о предоставлении открытой лицензии на право использования программы для ЭВМ Greensight Ecom Platform (GEP) (далее — Ensi) в соответствии с условиями ст. 1286.1 Гражданского кодекса РФ (далее — Оферта). -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +1.2. Правообладателем Ensi является ООО «ГринСайт» (далее — Правообладатель), в соответствии со свидетельством о государственной регистрации программы для ЭВМ № 2 020 663 096 от 22.10.2020 г. + +1.3. В соответствии с пунктом 2 статьи 437 Гражданского кодекса РФ в случае принятия изложенных в Оферте условий Правообладателя, юридическое или физическое лицо, производящее акцепт Оферты, становится лицензиатом (в соответствии с пунктом 3 статьи 438 ГК РФ акцепт оферты равносилен заключению договора на условиях, изложенных в оферте), а Правообладатель и лицензиат совместно — сторонами лицензионного договора. + +1.4. Вся документация, функциональные задания, сервисы и исходные коды Ensi размещены в сети Интернет по адресам: https://ensi.tech/ https://gitlab.com/greensight/ensi (далее — Сайт платформы). + +1.5. Правообладатель является участником проекта «Сколково» и предоставляет права использования Ensi в рамках коммерциализации результатов своих исследований и разработок по направлению «стратегические компьютерные технологии и программное обеспечение». + +2. Порядок акцепта оферты + +2.1. Лицензионный договор, заключаемый на основании акцептирования лицензиатом Оферты (далее — Лицензионный договор), является договором присоединения, к которому лицензиат присоединяется без каких-либо исключений и/или оговорок. + +2.2. Акцепт Оферты происходит в момент скачивания материалов Ensi с Сайта платформы. + +2.3. Срок акцепта Оферты не ограничен. + +3. Перечень прав использования Ensi + +3.1. При соблюдении лицензиатом требований раздела 4 Оферты, предоставляемое право использования ENSI включает в себя: + +3.1.1. Право использования Ensi на технических средствах лицензиата в соответствии с назначением Ensi, в том числе, все права использования, предусмотренные ст. 1280 Гражданского кодекса РФ; + +3.1.2. Право на воспроизведение Ensi, не ограниченное правом его инсталляции и запуска; + +3.1.3. Право на модификацию, адаптацию, внесение изменений и создание производных произведений (сложных произведений) с Ensi. + +3.2. Лицензиату предоставляется право передачи третьим лицам прав, указанных в п. 3.1 Оферты (право сублицензирования). + +3.3. Действие Лицензионного договора — территория всего мира. + +3.4. Право использования Ensi предоставляется лицензиату на весь срок действия исключительных прав Правообладателя. + +3.5. Право использования Ensi предоставляется безвозмездно. Лицензиат вправе использовать Ensi для создания производных произведений (сложных произведений) и их коммерческого применения, с учетом ограничений раздела 4 Оферты. + +4. Обязанности лицензиата + +4.1. Лицензиату предоставляются права указанные в разделе 3 Оферты при соблюдении им следующих условий: + +4.1.1. Наличия письменного указания на авторство Правообладателя и ссылки на Сайт платформы при реализации третьим лицам Ensi (в коммерческих или некоммерческих целях), а также в любых созданных производных от Ensi произведениях. + +4.1.2. Сохранения неизменным следующих частей кода Ensi: +- в файле src/pages/_app.tsx строка — <meta name="generator" content="Ensi Platform" /> +- в файле next.config.js строка — return [{ source: '/(.*)', headers: [{ key: 'X-Ensi-Platform', value: '1' }] }]; + +Удаление данных частей кода будет является существенным нарушением условий Оферты. + +4.1.3. Использования Ensi в законных целях, а именно: не нарушающих законодательство Российской Федерации, норм международных договоров Российской Федерации, общепризнанных принципов и норм международного права. Не допускается использование Ensi в проектах, противоречащих принципам гуманности и морали, в распространении материалов и информации запрещенных в Российской Федерации. + +4.2. При нарушении лицензиатом условий п. 4.1 Оферты, Правообладатель вправе в одностороннем порядке расторгнуть Лицензионный договор и потребовать мер защиты исключительных прав, включая положения ст.ст. 1252, 1301 Гражданского кодекса РФ. + +4.3. Лицензиат дает Правообладателю согласие на указание своего фирменного наименования и логотипа на сайте Платформы. Правообладатель вправе использовать фирменное наименование и логотип Лицензиата в своих маркетинговых целях без дополнительного согласования с Лицензиатом. + +5. Ограничение ответственности + +5.1. Права использования Ensi предоставляются на условии «как есть» («as is») без какого-либо вида гарантий. Правообладатель не имеет обязательств перед лицензиатом по поддержанию функционирования Ensi в случае сбоя в работе, обеспечению отказоустойчивости и иных параметров, позволяющих использовать Ensi. Правообладатель не несет ответственности за любые убытки, упущенную выгоду, связанную с повреждением имущества, неполученным доходом, прерыванием коммерческой или производственной деятельности, возникшие вследствие использования Ensi лицензиатом. + +6. Заключительные положения + +6.1. Оферта вступает в силу с даты ее размещения на Сайте платформы и действует до момента прекращения исключительных прав на Ensi у Правообладателя. + +6.2. Переход исключительного права на Ensi к новому правообладателю не будет являться основанием для изменения или расторжения Лицензионного договора. + +6.3. К отношениям между Правообладателем и лицензиатом применяется право Российской Федерации. + +6.4. Реквизиты Правообладателя: +Общество с ограниченной ответственностью «ГринСайт» +ОГРН 11 087 746 328 812 +ИНН 7 735 538 694 +КПП 773 501 001 \ No newline at end of file diff --git a/README.md b/README.md index dd127be..a968795 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,45 @@ # Laravel Auditing +[![Latest Version on Packagist](https://img.shields.io/packagist/v/ensi/laravel-auditing.svg?style=flat-square)](https://packagist.org/packages/ensi/laravel-auditing) +[![Tests](https://github.com/ensi-platform/laravel-auditing/actions/workflows/run-tests.yml/badge.svg?branch=master)](https://github.com/ensi-platform/laravel-auditing/actions/workflows/run-tests.yml) +[![Total Downloads](https://img.shields.io/packagist/dt/ensi/laravel-auditing.svg?style=flat-square)](https://packagist.org/packages/ensi/laravel-auditing) + Opiniated fork of [owen-it/laravel-auditing](https://github.com/owen-it/laravel-auditing) -## Установка +## Installation + +You can install the package via composer: + +```bash +composer require ensi/laravel-auditing +``` + +Publish the migrations with: + +```bash +php artisan vendor:publish --provider="Ensi\LaravelAuditing\LaravelAuditingServiceProvider" +``` -1. `composer require ensi/laravel-auditing` -2. `php artisan vendor:publish --provider="Ensi\LaravelAuditing\LaravelAuditingServiceProvider"` -3. Добавьте в `config/app` класс провайдера `Ensi\LaravelAuditing\LaravelAuditingServiceProvider::class` +### Migrate from 0.2.x to 0.3.0 -## Использование +1. Publish new migration `php artisan vendor:publish --provider="Ensi\LaravelAuditing\LaravelAuditingServiceProvider" --tag=migrations-0.3` +2. If the config `laravel-auditing.php` is published, then replace the `resolver.user`value with `Ensi\LaravelAuditing\Resolvers\UserResolver::class` -По-умолчанию никакая история изменения для моделей не сохраняется. -Чтобы включить логирование для конкретной модели надо добавить ей трейт `SupportsAudit` и интерфейс `Auditable` +## Version Compatibility + +| Laravel Auditing | Laravel | PHP | +|------------------|--------------------------------------|------| +| ^0.1.2 | ^7.x \|\| ^8.x | ^8.0 | +| ^0.2.0 | ^7.x \|\| ^8.x | ^8.0 | +| ^0.3.0 | ^7.x \|\| ^8.x | ^8.0 | +| ^0.3.1 | ^8.x \|\| ^9.x | ^8.0 | +| ^0.3.5 | ^8.x \|\| ^9.x \|\| ^10.x \|\| ^11.x | ^8.0 | +| ^0.4.0 | ^9.x \|\| ^10.x \|\| ^11.x | ^8.1 | + +## Basic Usage + +By default, no modification history is saved for models. +To enable logging for a specific model, you need to add the `Support s Audit` trait and the `Auditable` interface to it ```php use Ensi\LaravelAuditing\Contracts\Auditable; @@ -23,8 +51,8 @@ class Something extends Model implements Auditable { ``` -В случае, если мы меняем данные дочерних с логической точки зрения моделей и хотим чтобы в истории это изменение проходило под родительской моделью, необходимо в транзакции до изменения данных задать корневую сущность (т.е модель). -Делается это через фасад `Transaction` или менеджер `\\Ensi\\LaravelAuditing\\Transactions\\ExtendedTransactionManager` +If we change the data of the child models from a logical point of view and want this change to take place under the parent model in the history, it is necessary to set the root entity (i.e. the model) in the transaction before changing the data. +This is done through the `Transaction` facade or the manager `\\Ensi\\LaravelAuditing\\Transactions\\ExtendedTransactionManager` ```php DB::transaction(function () { @@ -34,35 +62,37 @@ DB::transaction(function () { }); ``` -Для добавления в историю данных о том кто произвел изменения (конкретный пользователь, или, например, консольная команда) опять же нужно это сделать до изменения данных, но уже через фасад `Subject` или инъекцию `\\Ensi\\LaravelAuditing\\Resolvers\\SubjectManager` +To add data to the history about who made the changes (a specific user, or, for example, a console command), again, you need to do this before changing the data, but through the `Subject` facade or the injection of `\\Ensi\\LaravelAuditing\\Resolvers\\SubjectManager` ```php Subject::attach($subject); // $subject - объект реализующий Ensi\LaravelAuditing\Contracts ``` -Субъект не отвязывается после завершения транзакции. -Его можно отвязать вручную вызовом метода `Subject::detach()`. +The subject does not unbind after the transaction is completed. +It can be unlinked manually by calling the `Subject::detach()` method. -При обработке http запросов, можно задавать субъекта в middleware. В консольных командах и -обработчиках очереди событий переназначать в процессе выполнения. +When processing http requests, you can set the subject in middleware. In console commands and handlers, event queues are reassigned during execution. -Субъектом может являться любая сущность, поддерживающая интерфейс `\Ensi\LaravelAuditing\Contracts\Principal`. -Если субъектом является выполняемое задание, например, импорт из файла, то оно может возвращать идентификатор -пользователя, создавшего задание в методе `getUserIdentifier()`, а в качестве наименования возвращать имя -импортируемого файла. +The subject can be any entity that supports the interface `\Ensi\LaravelAuditing\Contracts\Principal`. +If the subject is an ongoing task, for example, importing from a file, then it can return the ID of the user who created the task in the `getUserIdentifier()` method, and return the name of the imported file as the name. -В модели пользователя методы `getAuthIdentifier()` и `getUserIdentifier()` возвращают один и тот же идентификатор. +In the user model, the `getAuthIdentifier()` and `getUserIdentifier()` methods return the same identifier. -Также в отличии от исходного пакета в истории сохраняются не только измененные поля, но и полное состояние объекта модели на момент изменения. +Also, unlike the original package, not only the changed fields are saved in the history, but also the complete state of the model object at the time of the change. ## Contributing +Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. + ### Testing 1. composer install -2. npm i -3. composer test +2. composer test + +## Security Vulnerabilities + +Please review [our security policy](.github/SECURITY.md) on how to report security vulnerabilities. -## Лицензия +## License -[The MIT License (MIT)](LICENSE.md). +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/UPGRADE.md b/UPGRADE.md deleted file mode 100644 index 117dcb5..0000000 --- a/UPGRADE.md +++ /dev/null @@ -1,6 +0,0 @@ -# Upgrades - -## 0.2.x to 0.3.0 - -1. Publish new migration `php artisan vendor:publish --provider="Ensi\LaravelAuditing\LaravelAuditingServiceProvider" --tag=migrations-0.3` -2. If the config `laravel-auditing.php` is published, then replace the `resolver.user`value with `Ensi\LaravelAuditing\Resolvers\UserResolver::class` diff --git a/composer.json b/composer.json index 6ce9120..11f7a97 100644 --- a/composer.json +++ b/composer.json @@ -1,50 +1,52 @@ { - "name": "ensi/laravel-auditing", - "description": "", - "type": "library", - "license": "MIT", - "authors": [ - { - "name": "Alex Kroll", - "email": "crol@greensight.ru" + "name": "ensi/laravel-auditing", + "description": "laravel auditing", + "type": "library", + "license": "MIT", + "require": { + "php": "^8.1", + "laravel/framework": "^9.0 || ^10.0 || ^11.0", + "ramsey/uuid": "^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "nunomaduro/collision": "^6.0 || ^7.0 || ^8.1", + "pestphp/pest": "^1.22 || ^2.0", + "pestphp/pest-plugin-laravel": "^1.1 || ^2.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.11", + "spaze/phpstan-disallowed-calls": "^2.15", + "orchestra/testbench": "^7.0 || ^8.0 || ^9.0" + }, + "autoload": { + "psr-4": { + "Ensi\\LaravelAuditing\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Ensi\\LaravelAuditing\\Tests\\": "tests/" + } + }, + "scripts": { + "cs": "php-cs-fixer fix --config .php-cs-fixer.php", + "phpstan": "phpstan analyse", + "test": "./vendor/bin/pest --parallel --no-coverage", + "test-ci": "./vendor/bin/pest --no-coverage", + "test-coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --parallel --coverage" + }, + "extra": { + "laravel": { + "providers": [ + "Ensi\\LaravelAuditing\\LaravelAuditingServiceProvider" + ] + } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true + } } - ], - "require": { - "php": "^8.0", - "illuminate/console": "^8.0|^9.0|^10.0|^11.0", - "illuminate/database": "^8.0|^9.0|^10.0|^11.0", - "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0", - "ramsey/uuid": "^4.0" - }, - "autoload": { - "psr-4": { - "Ensi\\LaravelAuditing\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Ensi\\LaravelAuditing\\Tests\\": "tests/", - "Ensi\\LaravelAuditing\\Database\\Factories\\": "tests/database/factories/" - } - }, - "extra": { - "laravel": { - "providers": [ - "Ensi\\LaravelAuditing\\LaravelAuditingServiceProvider" - ] - } - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.2", - "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0", - "php-parallel-lint/php-var-dump-check": "^0.5.0", - "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0", - "mockery/mockery": "^1.0", - "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0" - }, - "scripts": { - "cs": "php-cs-fixer fix --config .php-cs-fixer.php", - "test": "./vendor/bin/phpunit" - } } diff --git a/package.json b/package.json deleted file mode 100644 index 3914640..0000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "private": true, - "devDependencies": { - "husky": "^4.3.0" - } -} diff --git a/phpstan-package.neon b/phpstan-package.neon new file mode 100644 index 0000000..e69de29 diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..a2205ac --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,61 @@ +includes: + - ./vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon + - ./phpstan-package.neon + +parameters: + paths: + - src + + scanFiles: + + # Pest handles loading custom helpers only when running tests + # @see https://pestphp.com/docs/helpers#usage + - tests/Pest.php + + # The level 9 is the highest level + level: 5 + + ignoreErrors: + - '#PHPDoc tag @var#' + + - '#Unsafe usage of new static\(\)\.#' + + # Pest implicitly binds $this to the current test case + # @see https://pestphp.com/docs/underlying-test-case + - + message: '#^Undefined variable: \$this$#' + path: '*Test.php' + + # Pest custom expectations are dynamic and not conducive static analysis + # @see https://pestphp.com/docs/expectations#custom-expectations + - + message: '#Call to an undefined method Pest\\Expectation|Pest\\Support\\Extendable::#' + path: '*Test.php' + + # Pest allow pass any array for TestCall::with + - + message: '#Parameter \#\d ...\$data of method Pest\\PendingCalls\\TestCall::with(.*) array(.*)given#' + path: '*Test.php' + + # Ignore custom method for Faker\Generator + - + message: '#Call to an undefined method Faker\\Generator|Ensi\\TestFactories\\FakerProvider::#' + path: '*Factory.php' + + # Ignore transfer of UploadedFile in auto-generated lib + - + message: '#expects SplFileObject\|null, Illuminate\\Http\\UploadedFile given.#' + path: '*Action.php' + + excludePaths: + - ./*/*/FileToBeExcluded.php + + disallowedFunctionCalls: + - + function: 'dd()' + message: 'use some logger instead' + - + function: 'dump()' + message: 'use some logger instead' + + reportUnmatchedIgnoredErrors: false diff --git a/phpunit.xml b/phpunit.xml index bedb86f..c2a4f58 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,26 +1,20 @@ - - ./tests + + tests - + - ./src + ./src + + + diff --git a/src/Audit.php b/src/Audit.php index eface13..726d548 100644 --- a/src/Audit.php +++ b/src/Audit.php @@ -71,22 +71,22 @@ public function resolveData(): array { // Metadata $this->data = [ - 'audit_id' => $this->id, - 'audit_event' => $this->event, - 'audit_url' => $this->url, - 'audit_ip_address' => $this->ip_address, - 'audit_user_agent' => $this->user_agent, - 'audit_tags' => $this->tags, - 'audit_created_at' => $this->serializeDate($this->created_at), - 'audit_updated_at' => $this->serializeDate($this->updated_at), - 'root_entity_id' => $this->getAttribute('root_entity_type'), - 'root_entity_type' => $this->getAttribute('root_entity_id'), - 'subject_id' => $this->getAttribute('subject_id'), - 'subject_type' => $this->getAttribute('subject_type'), - 'transaction_uid' => $this->getAttribute('transaction_uid'), - 'transaction_time' => $this->serializeDate($this->transaction_time ?? now()), - 'user_id' => $this->user_id, - 'extra' => $this->extra, + 'audit_id' => $this->id, + 'audit_event' => $this->event, + 'audit_url' => $this->url, + 'audit_ip_address' => $this->ip_address, + 'audit_user_agent' => $this->user_agent, + 'audit_tags' => $this->tags, + 'audit_created_at' => $this->serializeDate($this->created_at), + 'audit_updated_at' => $this->serializeDate($this->updated_at), + 'root_entity_id' => $this->getAttribute('root_entity_type'), + 'root_entity_type' => $this->getAttribute('root_entity_id'), + 'subject_id' => $this->getAttribute('subject_id'), + 'subject_type' => $this->getAttribute('subject_type'), + 'transaction_uid' => $this->getAttribute('transaction_uid'), + 'transaction_time' => $this->serializeDate($this->transaction_time ?? now()), + 'user_id' => $this->user_id, + 'extra' => $this->extra, ]; if ($this->subject && ($this->subject instanceof Principal)) { @@ -101,11 +101,11 @@ public function resolveData(): array // Modified Auditable attributes foreach ($this->new_values as $key => $value) { - $this->data['new_'.$key] = $value; + $this->data['new_' . $key] = $value; } foreach ($this->old_values as $key => $value) { - $this->data['old_'.$key] = $value; + $this->data['old_' . $key] = $value; } $this->modified = array_diff_key(array_keys($this->data), $this->metadata); @@ -252,6 +252,6 @@ public function getModified(bool $json = false, int $options = 0, int $depth = 5 */ public function getTags(): array { - return preg_split('/,/', $this->tags, null, PREG_SPLIT_NO_EMPTY); + return preg_split('/,/', $this->tags, flags: PREG_SPLIT_NO_EMPTY); } } diff --git a/src/Auditor.php b/src/Auditor.php index 3e378cc..f9008ad 100644 --- a/src/Auditor.php +++ b/src/Auditor.php @@ -66,9 +66,8 @@ public function execute(Auditable $model) return; } - if ($audit = $driver->audit($model)) { - $driver->prune($model); - } + $audit = $driver->audit($model); + $driver->prune($model); $this->container->make('events')->dispatch( new Audited($model, $driver, $audit) diff --git a/src/Console/AuditDriverCommand.php b/src/Console/AuditDriverCommand.php index 1e853ba..750b2f7 100644 --- a/src/Console/AuditDriverCommand.php +++ b/src/Console/AuditDriverCommand.php @@ -26,7 +26,7 @@ class AuditDriverCommand extends GeneratorCommand */ protected function getStub() { - return __DIR__.'/../../drivers/driver.stub'; + return __DIR__ . '/../../drivers/driver.stub'; } /** @@ -34,6 +34,6 @@ protected function getStub() */ protected function getDefaultNamespace($rootNamespace) { - return $rootNamespace.'\AuditDrivers'; + return $rootNamespace . '\AuditDrivers'; } } diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php deleted file mode 100644 index d687db8..0000000 --- a/src/Console/InstallCommand.php +++ /dev/null @@ -1,58 +0,0 @@ -comment('Publishing Auditing Configuration...'); - $this->callSilent('vendor:publish', ['--tag' => 'config']); - - $this->comment('Publishing Auditing Migrations...'); - $this->callSilent('vendor:publish', ['--tag' => 'migrations']); - - $this->registerAuditingServiceProvider(); - - $this->info('Auditing installed successfully.'); - } - - /** - * Register the Auditing service provider in the application configuration file. - * - * @return void - */ - protected function registerAuditingServiceProvider() - { - $namespace = Str::replaceLast('\\', '', Container::getInstance()->getNamespace()); - - $appConfig = file_get_contents(config_path('app.php')); - - if (Str::contains($appConfig, 'Ensi\\LaravelAuditing\\LaravelAuditingServiceProvider::class')) { - return; - } - - file_put_contents(config_path('app.php'), str_replace( - "{$namespace}\\Providers\EventServiceProvider::class,".PHP_EOL, - "{$namespace}\\Providers\EventServiceProvider::class,".PHP_EOL." Ensi\LaravelAuditing\LaravelAuditingServiceProvider::class,".PHP_EOL, - $appConfig - )); - } -} diff --git a/src/Contracts/AuditDriver.php b/src/Contracts/AuditDriver.php index 3b67e80..34b1a47 100644 --- a/src/Contracts/AuditDriver.php +++ b/src/Contracts/AuditDriver.php @@ -7,16 +7,16 @@ interface AuditDriver /** * Perform an audit. * - * @param \Ensi\LaravelAuditing\Contracts\Auditable $model + * @param Auditable $model * - * @return \Ensi\LaravelAuditing\Contracts\Audit + * @return Audit */ public function audit(Auditable $model): Audit; /** * Remove older audits that go over the threshold. * - * @param \Ensi\LaravelAuditing\Contracts\Auditable $model + * @param Auditable $model * * @return bool */ diff --git a/src/Contracts/Auditable.php b/src/Contracts/Auditable.php index 197871c..0a93e18 100644 --- a/src/Contracts/Auditable.php +++ b/src/Contracts/Auditable.php @@ -2,16 +2,19 @@ namespace Ensi\LaravelAuditing\Contracts; +use Ensi\LaravelAuditing\Exceptions\AuditableTransitionException; +use Ensi\LaravelAuditing\Exceptions\AuditingException; use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Database\Query\Builder; interface Auditable { /** * Auditable Model audits. * - * @return \Illuminate\Database\Eloquent\Relations\MorphMany + * @return MorphMany|Builder */ - public function audits(): MorphMany; + public function audits(): MorphMany|Builder; /** * Set the Audit event. @@ -46,7 +49,7 @@ public function readyForAuditing(): bool; /** * Return data for an Audit. * - * @throws \Ensi\LaravelAuditing\Exceptions\AuditingException + * @throws AuditingException * * @return array */ @@ -130,7 +133,7 @@ public function getAuditExtra(): ?array; * @param Audit $audit * @param bool $old * - * @throws \Ensi\LaravelAuditing\Exceptions\AuditableTransitionException + * @throws AuditableTransitionException * * @return Auditable */ diff --git a/src/Facades/Subject.php b/src/Facades/Subject.php index fddd8a2..a9b563f 100644 --- a/src/Facades/Subject.php +++ b/src/Facades/Subject.php @@ -2,7 +2,6 @@ namespace Ensi\LaravelAuditing\Facades; -use Ensi\LaravelAuditing\Contracts\Principal; use Ensi\LaravelAuditing\Resolvers\SubjectManager; use Illuminate\Support\Facades\Facade; use Illuminate\Support\Optional; @@ -18,9 +17,6 @@ protected static function getFacadeAccessor(): string return SubjectManager::class; } - /** - * @return Optional|Principal - */ public static function resolve(): Optional { return static::getFacadeRoot()->current(); diff --git a/src/LaravelAuditingServiceProvider.php b/src/LaravelAuditingServiceProvider.php index 064cfa0..9d549cb 100644 --- a/src/LaravelAuditingServiceProvider.php +++ b/src/LaravelAuditingServiceProvider.php @@ -3,7 +3,6 @@ namespace Ensi\LaravelAuditing; use Ensi\LaravelAuditing\Console\AuditDriverCommand; -use Ensi\LaravelAuditing\Console\InstallCommand; use Ensi\LaravelAuditing\Contracts\Auditor; use Ensi\LaravelAuditing\Drivers\Database; use Ensi\LaravelAuditing\Facades\Subject; @@ -19,69 +18,53 @@ class LaravelAuditingServiceProvider extends ServiceProvider { - /** - * Bootstrap the service provider. - * - * @return void - */ - public function boot() - { - $this->registerPublishing(); - $this->registerListeners(); - - $this->mergeConfigFrom(__DIR__.'/../config/laravel-auditing.php', 'laravel-auditing'); - } - - /** - * Register the service provider. - * - * @return void - */ - public function register() + public function register(): void { - $this->commands([ - AuditDriverCommand::class, - InstallCommand::class, - ]); + $this->mergeConfigFrom(__DIR__ . '/../config/laravel-auditing.php', 'laravel-auditing'); - $this->app->singleton(TransactionRegistry::class, function (Application $app) { + $this->app->scoped(TransactionRegistry::class, function (Application $app) { return new TransactionRegistry($app['config']['database.default']); }); - $this->app->singleton(Database::class, function (Application $app) { + $this->app->scoped(Database::class, function (Application $app) { return new Database($app->make(TransactionRegistry::class)); }); - $this->app->singleton(Auditor::class, function (Application $app) { + $this->app->scoped(Auditor::class, function (Application $app) { return new \Ensi\LaravelAuditing\Auditor($app); }); - $this->app->singleton(SubjectManager::class); - $this->app->singleton(Subject::class); - $this->app->singleton(Transaction::class); + $this->app->scoped(SubjectManager::class); + $this->app->scoped(Subject::class); + $this->app->scoped(Transaction::class); } - /** - * Register the package's publishable resources. - * - * @return void - */ - private function registerPublishing() + public function boot(): void + { + $this->registerPublishing(); + $this->registerListeners(); + + $this->commands([ + AuditDriverCommand::class, + ]); + } + + private function registerPublishing(): void { if ($this->app->runningInConsole()) { $this->publishes([ - __DIR__.'/../config/laravel-auditing.php' => base_path('config/laravel-auditing.php'), + __DIR__ . '/../config/laravel-auditing.php' => base_path('config/laravel-auditing.php'), ], 'config'); $this->publishes([ - __DIR__.'/../database/migrations/audits.stub' => database_path( + __DIR__ . '/../database/migrations/audits.stub' => database_path( sprintf('migrations/%s_create_audits_table.php', date('Y_m_d_His')) ), ], 'migrations'); $this->publishes([ - __DIR__.'/../database/migrations/audits_extra.stub' => database_path( + __DIR__ . '/../database/migrations/audits_extra.stub' => database_path( sprintf('migrations/%s_push_audits_extra.php', date('Y_m_d_His')) ), ], 'migrations-0.3'); diff --git a/src/Models/Audit.php b/src/Models/Audit.php index a93aef5..abbbd67 100644 --- a/src/Models/Audit.php +++ b/src/Models/Audit.php @@ -2,6 +2,9 @@ namespace Ensi\LaravelAuditing\Models; +use Carbon\CarbonInterface; +use Ensi\LaravelAuditing\Contracts\Auditable; +use Ensi\LaravelAuditing\Contracts\Principal; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; @@ -17,25 +20,25 @@ * @property string $tags * * @property string $auditable_type - * @property int $auditable_id Измененная сущность + * @property int $auditable_id Измененная сущность * @property string $root_entity_type - * @property int $root_entity_id Корневая сущность + * @property int $root_entity_id Корневая сущность * @property string $subject_type - * @property int $subject_id Субъект доступа - * @property string|null $user_id Идентификатор пользователя - * @property array|null $extra Дополнительная информация + * @property int $subject_id Субъект доступа + * @property string|null $user_id Идентификатор пользователя + * @property array|null $extra Дополнительная информация * - * @property \Carbon\CarbonInterface $created_at - * @property \Carbon\CarbonInterface $updated_at + * @property CarbonInterface $created_at + * @property CarbonInterface $updated_at * @property string $transaction_uid - * @property \Carbon\CarbonInterface $transaction_time + * @property CarbonInterface $transaction_time * - * @property \Illuminate\Database\Eloquent\Model|\Ensi\LaravelAuditing\Contracts\Principal|null $subject - * @property \Illuminate\Database\Eloquent\Model|\Ensi\LaravelAuditing\Contracts\Auditable $auditable - * @property \Illuminate\Database\Eloquent\Model|null $root + * @property Model|Principal|null $subject + * @property Model|Auditable|null $auditable + * @property Model|null $root * * @method static static create(array $attributes) - * @method static \Illuminate\Database\Eloquent\Builder|static forRoot(\Illuminate\Database\Eloquent\Model $root) + * @method static Builder|static forRoot(Model $root) */ class Audit extends Model implements \Ensi\LaravelAuditing\Contracts\Audit { @@ -50,12 +53,12 @@ class Audit extends Model implements \Ensi\LaravelAuditing\Contracts\Audit * {@inheritdoc} */ protected $casts = [ - 'old_values' => 'json', - 'new_values' => 'json', + 'old_values' => 'json', + 'new_values' => 'json', // Note: Please do not add 'auditable_id' in here, as it will break non-integer PK models - 'state' => 'json', - 'extra' => 'json', - 'subject_id' => 'int', + 'state' => 'json', + 'extra' => 'json', + 'subject_id' => 'int', ]; protected $dates = ['transaction_time']; diff --git a/src/Resolvers/SubjectManager.php b/src/Resolvers/SubjectManager.php index 8421564..ec17b15 100644 --- a/src/Resolvers/SubjectManager.php +++ b/src/Resolvers/SubjectManager.php @@ -7,12 +7,8 @@ class SubjectManager { - /** @var Principal|null */ - private $principal; + private ?Principal $principal = null; - /** - * @return Optional|Principal - */ public function current(): Optional { return new Optional($this->principal); diff --git a/src/SupportsAudit.php b/src/SupportsAudit.php index 0545fb4..a35ded6 100644 --- a/src/SupportsAudit.php +++ b/src/SupportsAudit.php @@ -14,6 +14,7 @@ use Ensi\LaravelAuditing\Facades\Subject; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Query\Builder; use Illuminate\Support\Arr; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; @@ -56,7 +57,7 @@ public static function bootSupportsAudit() /** * {@inheritdoc} */ - public function audits(): MorphMany + public function audits(): MorphMany|Builder { return $this->morphMany( Config::get('laravel-auditing.implementation', Models\Audit::class), @@ -295,20 +296,20 @@ public function toAudit(): array $subject = $this->resolveSubject(); return $this->transformAudit([ - 'old_values' => $old, - 'new_values' => $new, - 'state' => $state, - 'event' => $this->auditEvent, - 'auditable_id' => $this->getKey(), - 'auditable_type' => $this->getMorphClass(), - 'subject_id' => $subject?->getAuthIdentifier(), - 'subject_type' => $subject?->getMorphClass(), - 'url' => $this->resolveUrl(), - 'ip_address' => $this->resolveIpAddress(), - 'user_agent' => $this->resolveUserAgent(), - 'user_id' => $this->resolveUser(), - 'tags' => empty($tags) ? null : $tags, - 'extra' => $this->getAuditExtra(), + 'old_values' => $old, + 'new_values' => $new, + 'state' => $state, + 'event' => $this->auditEvent, + 'auditable_id' => $this->getKey(), + 'auditable_type' => $this->getMorphClass(), + 'subject_id' => $subject?->getAuthIdentifier(), + 'subject_type' => $subject?->getMorphClass(), + 'url' => $this->resolveUrl(), + 'ip_address' => $this->resolveIpAddress(), + 'user_agent' => $this->resolveUserAgent(), + 'user_id' => $this->resolveUser(), + 'tags' => empty($tags) ? null : $tags, + 'extra' => $this->getAuditExtra(), ]); } diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index f935021..0000000 --- a/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!.gitignore diff --git a/tests/Data/Drivers/FakeDriver.php b/tests/Data/Drivers/FakeDriver.php new file mode 100644 index 0000000..cbcf985 --- /dev/null +++ b/tests/Data/Drivers/FakeDriver.php @@ -0,0 +1,7 @@ + Uuid::uuid4(), - 'content' => $this->faker->unique()->paragraph(6), + 'content' => $this->faker->unique()->paragraph(6), 'published_at' => null, ]; } diff --git a/tests/database/factories/ArticleFactory.php b/tests/Data/Models/Factories/ArticleFactory.php similarity index 59% rename from tests/database/factories/ArticleFactory.php rename to tests/Data/Models/Factories/ArticleFactory.php index 20f6666..77882fb 100644 --- a/tests/database/factories/ArticleFactory.php +++ b/tests/Data/Models/Factories/ArticleFactory.php @@ -1,7 +1,8 @@ $this->faker->unique()->sentence, - 'content' => $this->faker->unique()->paragraph(6), + 'title' => $this->faker->unique()->sentence, + 'content' => $this->faker->unique()->paragraph(6), 'published_at' => null, - 'reviewed' => $this->faker->randomElement([0, 1]), + 'reviewed' => $this->faker->randomElement([0, 1]), ]; } } diff --git a/tests/database/factories/AuditFactory.php b/tests/Data/Models/Factories/AuditFactory.php similarity index 57% rename from tests/database/factories/AuditFactory.php rename to tests/Data/Models/Factories/AuditFactory.php index 46855ac..62f1e2a 100644 --- a/tests/database/factories/AuditFactory.php +++ b/tests/Data/Models/Factories/AuditFactory.php @@ -1,9 +1,10 @@ function () { return User::factory()->create()->id; }, - 'subject_type' => User::class, - 'event' => 'updated', + 'subject_type' => User::class, + 'event' => 'updated', 'auditable_id' => function () { return Article::factory()->create()->id; }, 'auditable_type' => Article::class, - 'old_values' => [], - 'new_values' => [], - 'url' => $this->faker->url, - 'ip_address' => $this->faker->ipv4, - 'user_agent' => $this->faker->userAgent, - 'tags' => implode(',', $this->faker->words(4)), + 'old_values' => [], + 'new_values' => [], + 'url' => $this->faker->url, + 'ip_address' => $this->faker->ipv4, + 'user_agent' => $this->faker->userAgent, + 'tags' => implode(',', $this->faker->words(4)), ]; } } diff --git a/tests/database/factories/UserFactory.php b/tests/Data/Models/Factories/UserFactory.php similarity index 59% rename from tests/database/factories/UserFactory.php rename to tests/Data/Models/Factories/UserFactory.php index 2865ce9..510db7d 100644 --- a/tests/database/factories/UserFactory.php +++ b/tests/Data/Models/Factories/UserFactory.php @@ -1,7 +1,8 @@ $this->faker->randomElement([0, 1]), + 'is_admin' => $this->faker->randomElement([0, 1]), 'first_name' => $this->faker->firstName, - 'last_name' => $this->faker->lastName, - 'email' => $this->faker->unique()->safeEmail, - 'password' => $this->faker->password(), + 'last_name' => $this->faker->lastName, + 'email' => $this->faker->unique()->safeEmail, + 'password' => $this->faker->password(), ]; } } diff --git a/tests/Models/User.php b/tests/Data/Models/User.php similarity index 90% rename from tests/Models/User.php rename to tests/Data/Models/User.php index 1504248..4d98d8f 100644 --- a/tests/Models/User.php +++ b/tests/Data/Models/User.php @@ -1,11 +1,11 @@ app['config']->set('laravel-auditing.console', false); - - User::factory()->create(); - - $this->assertSame(1, User::query()->count()); - $this->assertSame(0, Audit::query()->count()); - } - - /** - * @test - */ - public function itWillAuditModelsWhenRunningFromTheConsole() - { - $this->app['config']->set('laravel-auditing.console', true); - - User::factory()->create(); - - $this->assertSame(1, User::query()->count()); - $this->assertSame(1, Audit::query()->count()); - } - - /** - * @test - */ - public function itWillAlwaysAuditModelsWhenNotRunningFromTheConsole() - { - App::shouldReceive('runningInConsole') - ->andReturn(false); - - $this->app['config']->set('laravel-auditing.console', false); - - User::factory()->create(); - - $this->assertSame(1, User::query()->count()); - $this->assertSame(1, Audit::query()->count()); - } - - /** - * @test - */ - public function itWillNotAuditTheRetrievingEvent() - { - $this->app['config']->set('laravel-auditing.console', true); - - User::factory()->create(); - - $this->assertSame(1, User::query()->count()); - $this->assertSame(1, Audit::query()->count()); - - User::first(); - - $this->assertSame(1, Audit::query()->count()); - $this->assertSame(1, User::query()->count()); - } - - /** - * @test - */ - public function itWillAuditTheRetrievingEvent() - { - $this->app['config']->set('laravel-auditing.console', true); - $this->app['config']->set('laravel-auditing.events', [ - 'created', - 'retrieved', - ]); - - User::factory()->create(); - - $this->assertSame(1, User::query()->count()); - $this->assertSame(1, Audit::query()->count()); - - User::first(); - - $this->assertSame(1, User::query()->count()); - $this->assertSame(2, Audit::query()->count()); - } - - /** - * @test - */ - public function itWillAuditTheRetrievedEvent() - { - $this->app['config']->set('laravel-auditing.events', [ - 'retrieved', - ]); - - Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ]); - - Article::first(); - - $audit = Audit::first(); - - $this->assertEmpty($audit->old_values); - - $this->assertEmpty($audit->new_values); - } - - /** - * @test - */ - public function itWillAuditTheCreatedEvent() - { - $this->app['config']->set('laravel-auditing.events', [ - 'created', - ]); - - Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ]); - - $audit = Audit::first(); - - $this->assertEmpty($audit->old_values); - - Assert::assertArraySubset([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ], $audit->new_values, true); - } - - /** - * @test - */ - public function itWillAuditTheUpdatedEvent() - { - $this->app['config']->set('laravel-auditing.events', [ - 'updated', - ]); - - $article = Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ]); - - $now = Carbon::now(); - - $article->update([ - 'content' => 'First step: install the laravel-auditing package.', - 'published_at' => $now, - 'reviewed' => 1, - ]); - - $audit = Audit::first(); - - Assert::assertArraySubset([ - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ], $audit->old_values, true); - - Assert::assertArraySubset([ - 'content' => 'First step: install the laravel-auditing package.', - 'published_at' => $now->toDateTimeString(), - 'reviewed' => 1, - ], $audit->new_values, true); - } - - /** - * @test - */ - public function itWillAuditTheDeletedEvent() - { - $this->app['config']->set('laravel-auditing.events', [ - 'deleted', - ]); - - $article = Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ]); - - $article->delete(); - - $audit = Audit::first(); - - Assert::assertArraySubset([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ], $audit->old_values, true); - - $this->assertEmpty($audit->new_values); - } - - /** - * @test - */ - public function itWillAuditTheRestoredEvent() - { - $this->app['config']->set('laravel-auditing.events', [ - 'restored', - ]); - - $article = Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ]); - - $article->delete(); - $article->restore(); - - $audit = Audit::first(); - - $this->assertEmpty($audit->old_values); - - Assert::assertArraySubset([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ], $audit->new_values, true); - } - - /** - * @test - */ - public function itWillKeepAllAudits() - { - $this->app['config']->set('laravel-auditing.threshold', 0); - $this->app['config']->set('laravel-auditing.events', [ - 'updated', - ]); - - $article = Article::factory()->create([ - 'reviewed' => 1, - ]); - - foreach (range(0, 99) as $count) { - $article->update([ - 'reviewed' => ($count % 2), - ]); - } - - $this->assertSame(100, $article->audits()->count()); - } - - /** - * @test - */ - public function itWillRemoveOlderAuditsAboveTheThreshold() - { - $this->app['config']->set('laravel-auditing.threshold', 10); - $this->app['config']->set('laravel-auditing.events', [ - 'updated', - ]); - - $article = Article::factory()->create([ - 'reviewed' => 1, - ]); - - foreach (range(0, 99) as $count) { - $article->update([ - 'reviewed' => ($count % 2), - ]); - } - - $this->assertSame(10, $article->audits()->count()); - } - - /** - * @test - */ - public function itWillNotAuditDueToUnsupportedDriver() - { - $this->app['config']->set('laravel-auditing.driver', 'foo'); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Driver [foo] not supported.'); - - Article::factory()->create(); - } - - /** - * @test - */ - public function itWillNotAuditDueToClassWithoutDriverInterface() - { - // We just pass a FQCN that does not implement the AuditDriver interface - $this->app['config']->set('laravel-auditing.driver', FakeDriver::class); - - $this->expectException(AuditingException::class); - $this->expectExceptionMessage('The driver must implement the AuditDriver contract'); - - Article::factory()->create(); - } - - /** - * @test - */ - public function itWillAuditUsingTheDefaultDriver() - { - $this->app['config']->set('laravel-auditing.driver', null); - - Article::factory()->create([ - 'title' => 'How To Audit Using The Fallback Driver', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ]); - - $audit = Audit::first(); - - $this->assertEmpty($audit->old_values); - - Assert::assertArraySubset([ - 'title' => 'How To Audit Using The Fallback Driver', - 'content' => 'N/A', - 'published_at' => null, - 'reviewed' => 0, - ], $audit->new_values, true); - } - - /** - * @test - */ - public function itWillCancelTheAuditFromAnEventListener() - { - Event::listen(Auditing::class, function () { - return false; - }); - - Article::factory()->create(); - - $this->assertNull(Audit::first()); - } - - /** - * @test - */ - public function itDisablesAndEnablesAuditingBackAgain() - { - // Auditing is enabled by default - $this->assertFalse(Article::$auditingDisabled); - - Article::factory()->create(); - - $this->assertSame(1, Article::count()); - $this->assertSame(1, Audit::count()); - - // Disable Auditing - Article::disableAuditing(); - $this->assertTrue(Article::$auditingDisabled); - - Article::factory()->create(); - - $this->assertSame(2, Article::count()); - $this->assertSame(1, Audit::count()); - - // Re-enable Auditing - Article::enableAuditing(); - $this->assertFalse(Article::$auditingDisabled); - - Article::factory()->create(); - - $this->assertSame(2, Audit::count()); - $this->assertSame(3, Article::count()); - } - - /** - * @test - */ - public function itAddsTransactionAttributesToAudit(): void - { - DB::transaction(function () { - /** @var Audit $audit */ - $audit = ArticleFactory::new()->create()->audits()->first(); - - $this->assertEquals($audit->transaction_uid, Transaction::uid()->toString()); - $this->assertEquals($audit->transaction_time, Transaction::timestamp()); - }); - } - - /** - * @test - */ - public function itAddsRootEntityToAudit(): void - { - /** @var Article $article */ - $article = ArticleFactory::new()->create(); - - DB::transaction(function () use ($article) { - Transaction::setRootEntity($article); - - /** @var Audit $audit */ - $audit = ApiModelFactory::new()->create()->audits()->first(); - - $this->assertEquals($article->getKey(), $audit->root_entity_id); - $this->assertEquals($article->getMorphClass(), $audit->root_entity_type); - }); - } -} diff --git a/tests/Functional/CommandTest.php b/tests/Functional/CommandTest.php index 05853da..a217480 100644 --- a/tests/Functional/CommandTest.php +++ b/tests/Functional/CommandTest.php @@ -1,34 +1,26 @@ app->path() - ); +use function Pest\Laravel\artisan; +use function PHPUnit\Framework\assertFileExists; +use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertTrue; + +uses(TestCase::class); + +test('generate the audit driver success', function () { + /** @var TestCase $this */ - $this->assertInstanceOf( - PendingCommand::class, - $this->artisan( - 'auditing:audit-driver', - [ - 'name' => 'TestDriver', - ] - ) - ); + assertInstanceOf( + PendingCommand::class, + artisan(AuditDriverCommand::class, ['name' => 'TestDriver']) + ); - $this->assertFileExists($driverFilePath); + $driverFilePath = app_path('AuditDrivers/TestDriver.php'); - $this->assertTrue(unlink($driverFilePath)); - } -} + assertFileExists($driverFilePath); + assertTrue(unlink($driverFilePath)); +}); diff --git a/tests/Functional/Test.php b/tests/Functional/Test.php new file mode 100644 index 0000000..ef126be --- /dev/null +++ b/tests/Functional/Test.php @@ -0,0 +1,415 @@ +app['config']->set('laravel-auditing.console', false); + + User::factory()->create(); + + assertSame(1, User::query()->count()); + assertSame(0, Audit::query()->count()); +}); + +test('it will audit models when running from the console', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.console', true); + + User::factory()->create(); + + assertSame(1, User::query()->count()); + assertSame(1, Audit::query()->count()); +}); + + +test('it will always audit models when not running from the console', function () { + /** @var TestCase $this */ + + App::shouldReceive('runningInConsole') + ->andReturn(false); + + $this->app['config']->set('laravel-auditing.console', false); + + User::factory()->create(); + + assertSame(1, User::query()->count()); + assertSame(1, Audit::query()->count()); +}); + + +test('it will not audit the retrieving event', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.console', true); + + User::factory()->create(); + + assertSame(1, User::query()->count()); + assertSame(1, Audit::query()->count()); + + User::first(); + + assertSame(1, Audit::query()->count()); + assertSame(1, User::query()->count()); +}); + + +test('it will audit the retrieving event', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.console', true); + $this->app['config']->set('laravel-auditing.events', [ + 'created', + 'retrieved', + ]); + + User::factory()->create(); + + assertSame(1, User::query()->count()); + assertSame(1, Audit::query()->count()); + + User::first(); + + assertSame(1, User::query()->count()); + assertSame(2, Audit::query()->count()); +}); + + +test('it will audit the retrieved event', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.events', [ + 'retrieved', + ]); + + Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ]); + + Article::first(); + + $audit = Audit::first(); + + assertEmpty($audit->old_values); + + assertEmpty($audit->new_values); +}); + + +test('it will audit the created event', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.events', [ + 'created', + ]); + + Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ]); + + $audit = Audit::first(); + + assertEmpty($audit->old_values); + + Assert::assertArraySubset([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ], $audit->new_values, true); +}); + + +test('it will audit the updated event', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.events', [ + 'updated', + ]); + + $article = Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ]); + + $now = Carbon::now(); + + $article->update([ + 'content' => 'First step: install the laravel-auditing package.', + 'published_at' => $now, + 'reviewed' => 1, + ]); + + $audit = Audit::first(); + + Assert::assertArraySubset([ + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ], $audit->old_values, true); + + Assert::assertArraySubset([ + 'content' => 'First step: install the laravel-auditing package.', + 'published_at' => $now->toDateTimeString(), + 'reviewed' => 1, + ], $audit->new_values, true); +}); + + +test('it will audit the deleted event', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.events', [ + 'deleted', + ]); + + $article = Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ]); + + $article->delete(); + + $audit = Audit::first(); + + Assert::assertArraySubset([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ], $audit->old_values, true); + + assertEmpty($audit->new_values); +}); + + +test('it will audit the restored event', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.events', [ + 'restored', + ]); + + $article = Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ]); + + $article->delete(); + $article->restore(); + + $audit = Audit::first(); + + assertEmpty($audit->old_values); + + Assert::assertArraySubset([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ], $audit->new_values, true); +}); + + +test('it will keep all audits', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.threshold', 0); + $this->app['config']->set('laravel-auditing.events', [ + 'updated', + ]); + + $article = Article::factory()->create([ + 'reviewed' => 1, + ]); + + foreach (range(0, 99) as $count) { + $article->update([ + 'reviewed' => ($count % 2), + ]); + } + + assertSame(100, $article->audits()->count()); +}); + + +test('it will remove older audits above the threshold', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.threshold', 10); + $this->app['config']->set('laravel-auditing.events', [ + 'updated', + ]); + + $article = Article::factory()->create([ + 'reviewed' => 1, + ]); + + foreach (range(0, 99) as $count) { + $article->update([ + 'reviewed' => ($count % 2), + ]); + } + + assertSame(10, $article->audits()->count()); +}); + + +test('it will not audit due to unsupported driver', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.driver', 'foo'); + + Article::factory()->create(); +})->expectExceptionObject(new InvalidArgumentException('Driver [foo] not supported.')); + + +test('it will not audit due to class without driver interface', function () { + /** @var TestCase $this */ + + // We just pass a FQCN that does not implement the AuditDriver interface + $this->app['config']->set('laravel-auditing.driver', FakeDriver::class); + + Article::factory()->create(); +})->expectExceptionObject(new AuditingException('The driver must implement the AuditDriver contract')); + + +test('it will audit using the default driver', function () { + /** @var TestCase $this */ + + $this->app['config']->set('laravel-auditing.driver', null); + + Article::factory()->create([ + 'title' => 'How To Audit Using The Fallback Driver', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ]); + + $audit = Audit::first(); + + assertEmpty($audit->old_values); + + Assert::assertArraySubset([ + 'title' => 'How To Audit Using The Fallback Driver', + 'content' => 'N/A', + 'published_at' => null, + 'reviewed' => 0, + ], $audit->new_values, true); +}); + + +test('it will cancel the audit from an event listener', function () { + /** @var TestCase $this */ + + Event::listen(Auditing::class, function () { + return false; + }); + + Article::factory()->create(); + + assertNull(Audit::first()); +}); + + +test('it disables and enables auditing back again', function () { + /** @var TestCase $this */ + + // Auditing is enabled by default + assertFalse(Article::$auditingDisabled); + + Article::factory()->create(); + + assertSame(1, Article::count()); + assertSame(1, Audit::count()); + + // Disable Auditing + Article::disableAuditing(); + assertTrue(Article::$auditingDisabled); + + Article::factory()->create(); + + assertSame(2, Article::count()); + assertSame(1, Audit::count()); + + // Re-enable Auditing + Article::enableAuditing(); + assertFalse(Article::$auditingDisabled); + + Article::factory()->create(); + + assertSame(2, Audit::count()); + assertSame(3, Article::count()); +}); + + +test('it adds transaction attributes to audit', function () { + /** @var TestCase $this */ + + DB::transaction(function () { + /** @var Audit $audit */ + $audit = ArticleFactory::new()->create()->audits()->first(); + + assertEquals($audit->transaction_uid, Transaction::uid()->toString()); + assertEquals($audit->transaction_time, Transaction::timestamp()); + }); +}); + + +test('it adds root entity to audit', function () { + /** @var TestCase $this */ + + /** @var Article $article */ + $article = ArticleFactory::new()->create(); + + DB::transaction(function () use ($article) { + Transaction::setRootEntity($article); + + /** @var Audit $audit */ + $audit = ApiModelFactory::new()->create()->audits()->first(); + + assertEquals($article->getKey(), $audit->root_entity_id); + assertEquals($article->getMorphClass(), $audit->root_entity_type); + }); +}); diff --git a/tests/Functional/TransactionTest.php b/tests/Functional/TransactionTest.php index a665430..70c8405 100644 --- a/tests/Functional/TransactionTest.php +++ b/tests/Functional/TransactionTest.php @@ -2,88 +2,87 @@ namespace Ensi\LaravelAuditing\Tests\Functional; -use Ensi\LaravelAuditing\Database\Factories\ArticleFactory; use Ensi\LaravelAuditing\Facades\Transaction; -use Ensi\LaravelAuditing\Tests\AuditingTestCase; +use Ensi\LaravelAuditing\Tests\Data\Models\Factories\ArticleFactory; +use Ensi\LaravelAuditing\Tests\TestCase; use Illuminate\Support\Facades\DB; + +use function PHPUnit\Framework\assertEquals; +use function PHPUnit\Framework\assertFalse; +use function PHPUnit\Framework\assertNotEquals; +use function PHPUnit\Framework\assertNull; +use function PHPUnit\Framework\assertSame; +use function PHPUnit\Framework\assertTrue; + use RuntimeException; -class TransactionTest extends AuditingTestCase -{ - /** - * @test - */ - public function itHandlesBeginTransaction(): void - { - $uidBefore = Transaction::uid(); - DB::transaction(function () use ($uidBefore) { - $this->assertTrue(Transaction::isActive()); - $this->assertNotEquals($uidBefore, Transaction::uid()); - $this->assertEquals(Transaction::uid(), Transaction::uid()); - }); - } +uses(TestCase::class); - /** - * @test - */ - public function itHandlesCommit(): void - { - $uid = DB::transaction(function () { - return Transaction::uid(); - }); +test('it handles begin transaction', function () { + /** @var TestCase $this */ - $this->assertNotEquals($uid, Transaction::uid()); - $this->assertFalse(Transaction::isActive()); - } + $uidBefore = Transaction::uid(); + DB::transaction(function () use ($uidBefore) { + assertTrue(Transaction::isActive()); + assertNotEquals($uidBefore, Transaction::uid()); + assertEquals(Transaction::uid(), Transaction::uid()); + }); +}); - /** - * @test - */ - public function itHandlesRollback(): void - { - $uid = null; +test('it handles commit', function () { + /** @var TestCase $this */ - try { - DB::transaction(function () use (&$uid) { - $uid = Transaction::uid(); + $uid = DB::transaction(function () { + return Transaction::uid(); + }); - throw new RuntimeException('Failed'); - }); - } catch (RuntimeException) { - } + assertNotEquals($uid, Transaction::uid()); + assertFalse(Transaction::isActive()); +}); - $this->assertNotEquals($uid, Transaction::uid()); - $this->assertFalse(Transaction::isActive()); - } +test('it handles rollback', function () { + /** @var TestCase $this */ + + $uid = null; - /** - * @test - */ - public function itIgnoresNestedSavePoints(): void - { - DB::transaction(function () { + try { + DB::transaction(function () use (&$uid) { $uid = Transaction::uid(); - $timestamp = Transaction::timestamp(); - DB::transaction(function () use ($uid, $timestamp) { - $this->assertEquals($uid, Transaction::uid()); - $this->assertEquals($timestamp, Transaction::timestamp()); - }); + throw new RuntimeException('Failed'); }); + } catch (RuntimeException) { } - /** - * @test - */ - public function itRemembersRootEntity(): void - { - DB::transaction(function () { - $article = ArticleFactory::new()->create(); - Transaction::setRootEntity($article); + assertNotEquals($uid, Transaction::uid()); + assertFalse(Transaction::isActive()); +}); - $this->assertSame($article, Transaction::rootEntity()); + +test('it ignores nested save points', function () { + /** @var TestCase $this */ + + DB::transaction(function () { + $uid = Transaction::uid(); + $timestamp = Transaction::timestamp(); + + DB::transaction(function () use ($uid, $timestamp) { + assertEquals($uid, Transaction::uid()); + assertEquals($timestamp, Transaction::timestamp()); }); + }); +}); - $this->assertNull(Transaction::rootEntity()); - } -} + +test('it remembers root entity', function () { + /** @var TestCase $this */ + + DB::transaction(function () { + $article = ArticleFactory::new()->create(); + Transaction::setRootEntity($article); + + assertSame($article, Transaction::rootEntity()); + }); + + assertNull(Transaction::rootEntity()); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..febc07e --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,46 @@ +in(__DIR__); + + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +//expect()->extend('toBeOne', function () { +// return $this->toBe(1); +//}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +//function something() +//{ +// // .. +//} diff --git a/tests/AuditingTestCase.php b/tests/TestCase.php similarity index 76% rename from tests/AuditingTestCase.php rename to tests/TestCase.php index f3147e4..d7b9699 100644 --- a/tests/AuditingTestCase.php +++ b/tests/TestCase.php @@ -7,21 +7,25 @@ use Ensi\LaravelAuditing\Resolvers\UrlResolver; use Ensi\LaravelAuditing\Resolvers\UserAgentResolver; use Ensi\LaravelAuditing\Resolvers\UserResolver; -use Orchestra\Testbench\TestCase; +use Orchestra\Testbench\TestCase as Orchestra; -class AuditingTestCase extends TestCase +class TestCase extends Orchestra { - /** - * {@inheritdoc} - */ - protected function getEnvironmentSetUp($app) + protected function getPackageProviders($app): array + { + return [ + LaravelAuditingServiceProvider::class, + ]; + } + + protected function getEnvironmentSetUp($app): void { // Database $app['config']->set('database.default', 'testing'); $app['config']->set('database.connections.testing', [ - 'driver' => 'sqlite', + 'driver' => 'sqlite', 'database' => ':memory:', - 'prefix' => '', + 'prefix' => '', ]); $app['config']->set('auth.guards.api', [ 'driver' => 'session', @@ -42,23 +46,11 @@ protected function getEnvironmentSetUp($app) $app['config']->set('laravel-auditing.console', true); } - /** - * {@inheritdoc} - */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); - $this->loadMigrationsFrom(__DIR__ . '/database/migrations'); - } - - /** - * {@inheritdoc} - */ - protected function getPackageProviders($app) - { - return [ - LaravelAuditingServiceProvider::class, - ]; + $this->loadMigrationsFrom(__DIR__ . '/Data/database/migrations'); + $this->artisan('migrate')->run(); } } diff --git a/tests/Unit/AuditTest.php b/tests/Unit/AuditTest.php index b1f7ce1..2d13174 100644 --- a/tests/Unit/AuditTest.php +++ b/tests/Unit/AuditTest.php @@ -4,244 +4,215 @@ use Carbon\Carbon; use DateTimeInterface; -use Ensi\LaravelAuditing\Database\Factories\AuditFactory; use Ensi\LaravelAuditing\Encoders\Base64Encoder; use Ensi\LaravelAuditing\Facades\Subject; +use Ensi\LaravelAuditing\Models\Audit; use Ensi\LaravelAuditing\Redactors\LeftRedactor; -use Ensi\LaravelAuditing\Tests\AuditingTestCase; -use Ensi\LaravelAuditing\Tests\Models\Article; -use Ensi\LaravelAuditing\Tests\Models\User; -use Ensi\LaravelAuditing\Tests\Models\VirtualUser; -use Illuminate\Foundation\Testing\Concerns\InteractsWithTime; +use Ensi\LaravelAuditing\Tests\Data\Models\Article; +use Ensi\LaravelAuditing\Tests\Data\Models\Factories\AuditFactory; +use Ensi\LaravelAuditing\Tests\Data\Models\User; +use Ensi\LaravelAuditing\Tests\Data\Models\VirtualUser; +use Ensi\LaravelAuditing\Tests\TestCase; use Illuminate\Testing\Assert; -class AuditTest extends AuditingTestCase -{ - use InteractsWithTime; - - private const AUDIT_FIELDS_COUNT = 20; - private const AUDIT_META_FIELDS_COUNT = 16; - - /** - * @group Audit::resolveData - * @test - */ - public function itResolvesAuditData() - { - $now = Carbon::now(); - $article = null; - - $article = Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ]); - - $audit = $article->audits()->first(); - - $this->assertCount(self::AUDIT_FIELDS_COUNT, $resolvedData = $audit->resolveData()); - - Assert::assertArraySubset([ - 'audit_id' => 1, - 'audit_event' => 'created', - 'audit_url' => 'console', - 'audit_ip_address' => '127.0.0.1', - 'audit_user_agent' => 'Symfony', - 'audit_tags' => null, - 'audit_created_at' => $audit->created_at->toJSON(), - 'audit_updated_at' => $audit->updated_at->toJSON(), - 'new_title' => 'How To Audit Eloquent Models', - 'new_content' => 'First step: install the laravel-auditing package.', - 'new_published_at' => $now->toDateTimeString(), - 'new_reviewed' => 1, - 'extra' => ['year' => $now->year], - ], $resolvedData, true); - } - - /** - * @group Audit::resolveData - * @test - */ - public function itResolvesAuditDataIncludingSubjectAttributes() - { - $now = Carbon::now(); - - $user = User::factory()->create([ - 'is_admin' => 1, - 'first_name' => 'rick', - 'last_name' => 'Sanchez', - 'email' => 'rick@wubba-lubba-dub.dub', - ]); - - Subject::attach($user); - - $article = Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ]); - - $audit = $article->audits()->first(); - - $this->assertCount(self::AUDIT_FIELDS_COUNT + 1, $resolvedData = $audit->resolveData()); - - Assert::assertArraySubset([ - 'audit_id' => 2, - 'audit_event' => 'created', - 'audit_url' => 'console', - 'audit_ip_address' => '127.0.0.1', - 'audit_user_agent' => 'Symfony', - 'audit_tags' => null, - 'audit_created_at' => $audit->created_at->toJSON(), - 'audit_updated_at' => $audit->updated_at->toJSON(), - 'new_title' => 'How To Audit Eloquent Models', - 'new_content' => 'First step: install the laravel-auditing package.', - 'new_published_at' => $now->toDateTimeString(), - 'new_reviewed' => 1, - 'subject_id' => $user->getKey(), - ], $resolvedData, true); - } - - /** - * @test - */ - public function itResolvesAuditDataIncludingUserId() - { - $this->actingAs(new VirtualUser(), 'api'); - - $article = Article::factory()->create(); - - $audit = $article->audits()->first(); - - Assert::assertArraySubset([ - 'user_id' => VirtualUser::ID, - ], $audit->resolveData(), true); - } - - /** - * @test - */ - public function itResolvesAuditDataIncludingDefaultExtra() - { - $article = Article::factory()->create(); - - $audit = $article->audits()->first(); - - Assert::assertArraySubset([ - 'extra' => null, - ], $audit->resolveData(), true); - } - - /** - * @group Audit::resolveData - * @group Audit::getDataValue - * @test - */ - public function itReturnsTheAppropriateAuditableDataValues() - { - $user = User::factory()->create([ - 'is_admin' => 1, - 'first_name' => 'rick', - 'last_name' => 'Sanchez', - 'email' => 'rick@wubba-lubba-dub.dub', - ]); - - $audit = Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => Carbon::now(), - ])->audits()->first(); - - // Resolve data, making it available to the getDataValue() method - $this->assertCount(self::AUDIT_FIELDS_COUNT, $audit->resolveData()); - - // Mutate value - $this->assertSame('HOW TO AUDIT ELOQUENT MODELS', $audit->getDataValue('new_title')); - - // Cast value - $this->assertTrue($audit->getDataValue('new_reviewed')); - - // Date value - $this->assertInstanceOf(DateTimeInterface::class, $audit->getDataValue('new_published_at')); - - // Original value - $this->assertSame('First step: install the laravel-auditing package.', $audit->getDataValue('new_content')); - - // Invalid value - $this->assertNull($audit->getDataValue('invalid_key')); - } - - /** - * @group Audit::getMetadata - * @test - */ - public function itReturnsAuditMetadataAsArray() - { - $audit = Article::factory()->create()->audits()->first(); - - $this->assertCount(self::AUDIT_META_FIELDS_COUNT, $metadata = $audit->getMetadata()); - - Assert::assertArraySubset([ - 'audit_id' => 1, - 'audit_event' => 'created', - 'audit_url' => 'console', - 'audit_ip_address' => '127.0.0.1', - 'audit_user_agent' => 'Symfony', - 'audit_tags' => null, - 'audit_created_at' => $audit->created_at->toJSON(), - 'audit_updated_at' => $audit->updated_at->toJSON(), - ], $metadata, true); - } - - /** - * @group Audit::getMetadata - * @test - */ - public function itReturnsAuditMetadataIncludingSubjectAttributesAsArray() - { - $user = User::factory()->create([ - 'is_admin' => 1, - 'first_name' => 'rick', - 'last_name' => 'Sanchez', - 'email' => 'rick@wubba-lubba-dub.dub', - ]); - - Subject::attach($user); - - $audit = Article::factory()->create()->audits()->first(); - - $this->assertCount(self::AUDIT_META_FIELDS_COUNT + 1, $metadata = $audit->getMetadata()); - - Assert::assertArraySubset([ - 'audit_id' => 2, - 'audit_event' => 'created', - 'audit_url' => 'console', - 'audit_ip_address' => '127.0.0.1', - 'audit_user_agent' => 'Symfony', - 'audit_tags' => null, - 'audit_created_at' => $audit->created_at->toJSON(), - 'audit_updated_at' => $audit->updated_at->toJSON(), - 'subject_id' => $user->getKey(), - ], $metadata, true); - } - - /** - * @group Audit::getMetadata - * @test - */ - public function itReturnsAuditMetadataAsJsonString() - { - $this->travel(-1)->minutes(); - $now = now()->toJSON(); - - $audit = Article::factory()->create()->audits()->first(); - - $metadata = $audit->getMetadata(true, JSON_PRETTY_PRINT); - - $expected = <<< EOF +use function PHPUnit\Framework\assertCount; +use function PHPUnit\Framework\assertEmpty; +use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertIsArray; +use function PHPUnit\Framework\assertNull; +use function PHPUnit\Framework\assertSame; +use function PHPUnit\Framework\assertTrue; + +uses(TestCase::class); + +test('it resolves audit data', function () { + $now = Carbon::now(); + + /** @var Article $article */ + $article = Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ]); + + $audit = $article->audits()->first(); + + assertCount(Article::RESOLVE_FIELDS_COUNT, $resolvedData = $audit->resolveData()); + + Assert::assertArraySubset([ + 'audit_id' => 1, + 'audit_event' => 'created', + 'audit_url' => 'console', + 'audit_ip_address' => '127.0.0.1', + 'audit_user_agent' => 'Symfony', + 'audit_tags' => null, + 'audit_created_at' => $audit->created_at->toJSON(), + 'audit_updated_at' => $audit->updated_at->toJSON(), + 'new_title' => 'How To Audit Eloquent Models', + 'new_content' => 'First step: install the laravel-auditing package.', + 'new_published_at' => $now->toDateTimeString(), + 'new_reviewed' => 1, + 'extra' => ['year' => $now->year], + ], $resolvedData, true); +}); + +test('it resolves audit data including subject attributes', function () { + $now = Carbon::now(); + + $user = User::factory()->create([ + 'is_admin' => 1, + 'first_name' => 'rick', + 'last_name' => 'Sanchez', + 'email' => 'rick@wubba-lubba-dub.dub', + ]); + + Subject::attach($user); + + /** @var Article $article */ + $article = Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ]); + + $audit = $article->audits()->first(); + + assertCount(Article::RESOLVE_FIELDS_COUNT + 1, $resolvedData = $audit->resolveData()); + + Assert::assertArraySubset([ + 'audit_id' => 2, + 'audit_event' => 'created', + 'audit_url' => 'console', + 'audit_ip_address' => '127.0.0.1', + 'audit_user_agent' => 'Symfony', + 'audit_tags' => null, + 'audit_created_at' => $audit->created_at->toJSON(), + 'audit_updated_at' => $audit->updated_at->toJSON(), + 'new_title' => 'How To Audit Eloquent Models', + 'new_content' => 'First step: install the laravel-auditing package.', + 'new_published_at' => $now->toDateTimeString(), + 'new_reviewed' => 1, + 'subject_id' => $user->getKey(), + ], $resolvedData, true); +}); + +test('it resolves audit data including user id', function () { + $this->actingAs(new VirtualUser(), 'api'); + + /** @var Article $article */ + $article = Article::factory()->create(); + + $audit = $article->audits()->first(); + + Assert::assertArraySubset([ + 'user_id' => VirtualUser::ID, + ], $audit->resolveData(), true); +}); + +test('it resolves audit data including default extra', function () { + /** @var Article $article */ + $article = Article::factory()->create(); + + $audit = $article->audits()->first(); + + Assert::assertArraySubset([ + 'extra' => null, + ], $audit->resolveData(), true); +}); + +test('it returns the appropriate auditable data values', function () { + $user = User::factory()->create([ + 'is_admin' => 1, + 'first_name' => 'rick', + 'last_name' => 'Sanchez', + 'email' => 'rick@wubba-lubba-dub.dub', + ]); + + /** @var Article $article */ + $audit = Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => Carbon::now(), + ])->audits()->first(); + + // Resolve data, making it available to the getDataValue() method + assertCount(Article::RESOLVE_FIELDS_COUNT, $audit->resolveData()); + + // Mutate value + assertSame('HOW TO AUDIT ELOQUENT MODELS', $audit->getDataValue('new_title')); + + // Cast value + assertTrue($audit->getDataValue('new_reviewed')); + + // Date value + assertInstanceOf(DateTimeInterface::class, $audit->getDataValue('new_published_at')); + + // Original value + assertSame('First step: install the laravel-auditing package.', $audit->getDataValue('new_content')); + + // Invalid value + assertNull($audit->getDataValue('invalid_key')); +}); + +test('it returns audit metadata as array', function () { + /** @var Article $article */ + $audit = Article::factory()->create()->audits()->first(); + + assertCount(Article::AUDIT_META_FIELDS_COUNT, $metadata = $audit->getMetadata()); + + Assert::assertArraySubset([ + 'audit_id' => 1, + 'audit_event' => 'created', + 'audit_url' => 'console', + 'audit_ip_address' => '127.0.0.1', + 'audit_user_agent' => 'Symfony', + 'audit_tags' => null, + 'audit_created_at' => $audit->created_at->toJSON(), + 'audit_updated_at' => $audit->updated_at->toJSON(), + ], $metadata, true); +}); + +test('it returns audit metadata including subject attributes as array', function () { + $user = User::factory()->create([ + 'is_admin' => 1, + 'first_name' => 'rick', + 'last_name' => 'Sanchez', + 'email' => 'rick@wubba-lubba-dub.dub', + ]); + + Subject::attach($user); + + /** @var Article $article */ + $audit = Article::factory()->create()->audits()->first(); + + assertCount(Article::AUDIT_META_FIELDS_COUNT + 1, $metadata = $audit->getMetadata()); + + Assert::assertArraySubset([ + 'audit_id' => 2, + 'audit_event' => 'created', + 'audit_url' => 'console', + 'audit_ip_address' => '127.0.0.1', + 'audit_user_agent' => 'Symfony', + 'audit_tags' => null, + 'audit_created_at' => $audit->created_at->toJSON(), + 'audit_updated_at' => $audit->updated_at->toJSON(), + 'subject_id' => $user->getKey(), + ], $metadata, true); +}); + +test('it returns audit metadata as json string', function () { + $this->travel(-1)->minutes(); + $now = now()->toJSON(); + + /** @var Article $article */ + $audit = Article::factory()->create()->audits()->first(); + + $metadata = $audit->getMetadata(true, JSON_PRETTY_PRINT); + + $expected = <<< EOF { "audit_id": 1, "audit_event": "created", @@ -262,33 +233,29 @@ public function itReturnsAuditMetadataAsJsonString() } EOF; - $this->assertSame($expected, $metadata); - } + assertSame($expected, $metadata); +}); - /** - * @group Audit::getMetadata - * @test - */ - public function itReturnsAuditMetadataIncludingSubjectAttributesAsJsonString() - { - $user = User::factory()->create([ - 'is_admin' => 1, - 'first_name' => 'rick', - 'last_name' => 'Sanchez', - 'email' => 'rick@wubba-lubba-dub.dub', - ]); +test('it returns audit metadata including subject attributes as json string', function () { + $user = User::factory()->create([ + 'is_admin' => 1, + 'first_name' => 'rick', + 'last_name' => 'Sanchez', + 'email' => 'rick@wubba-lubba-dub.dub', + ]); - Subject::attach($user); + Subject::attach($user); - $this->travel(-1)->minutes(); - $now = now()->toJSON(); - $userId = $user->getKey(); + $this->travel(-1)->minutes(); + $now = now()->toJSON(); + $userId = $user->getKey(); - $audit = Article::factory()->create()->audits()->first(); + /** @var Article $article */ + $audit = Article::factory()->create()->audits()->first(); - $metadata = $audit->getMetadata(true, JSON_PRETTY_PRINT); + $metadata = $audit->getMetadata(true, JSON_PRETTY_PRINT); - $expected = <<< EOF + $expected = <<< EOF { "audit_id": 2, "audit_event": "created", @@ -301,7 +268,7 @@ public function itReturnsAuditMetadataIncludingSubjectAttributesAsJsonString() "root_entity_id": null, "root_entity_type": null, "subject_id": $userId, - "subject_type": "Ensi\\\\LaravelAuditing\\\\Tests\\\\Models\\\\User", + "subject_type": "Ensi\\\\LaravelAuditing\\\\Tests\\\\Data\\\\Models\\\\User", "transaction_uid": null, "transaction_time": "$now", "user_id": $userId, @@ -310,61 +277,53 @@ public function itReturnsAuditMetadataIncludingSubjectAttributesAsJsonString() } EOF; - $this->assertSame($expected, $metadata); - } - - /** - * @group Audit::getModified - * @test - */ - public function itReturnsAuditableModifiedAttributesAsArray() - { - $now = Carbon::now()->milliseconds(0); - - $audit = Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ])->audits()->first(); - - $this->assertCount(4, $modified = $audit->getModified()); - - Assert::assertArraySubset([ - 'title' => [ - 'new' => 'HOW TO AUDIT ELOQUENT MODELS', - ], - 'content' => [ - 'new' => 'First step: install the laravel-auditing package.', - ], - 'published_at' => [ - 'new' => $now->toJSON(), - ], - 'reviewed' => [ - 'new' => true, - ], - ], $modified, true); - } - - /** - * @group Audit::getModified - * @test - */ - public function itReturnsAuditableModifiedAttributesAsJsonString() - { - $now = Carbon::now()->milliseconds(0); - $publishedAt = $now->toJSON(); - - $audit = Article::factory()->create([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ])->audits()->first(); - - $modified = $audit->getModified(true, JSON_PRETTY_PRINT); - - $expected = <<< EOF + assertSame($expected, $metadata); +}); + +test('it returns auditable modified attributes as array', function () { + $now = Carbon::now()->milliseconds(0); + + /** @var Article $article */ + $audit = Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ])->audits()->first(); + + assertCount(4, $modified = $audit->getModified()); + + Assert::assertArraySubset([ + 'title' => [ + 'new' => 'HOW TO AUDIT ELOQUENT MODELS', + ], + 'content' => [ + 'new' => 'First step: install the laravel-auditing package.', + ], + 'published_at' => [ + 'new' => $now->toJSON(), + ], + 'reviewed' => [ + 'new' => true, + ], + ], $modified, true); +}); + +test('it returns auditable modified attributes as json string', function () { + $now = Carbon::now()->milliseconds(0); + $publishedAt = $now->toJSON(); + + /** @var Article $article */ + $audit = Article::factory()->create([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ])->audits()->first(); + + $modified = $audit->getModified(true, JSON_PRETTY_PRINT); + + $expected = <<< EOF { "title": { "new": "HOW TO AUDIT ELOQUENT MODELS" @@ -381,86 +340,72 @@ public function itReturnsAuditableModifiedAttributesAsJsonString() } EOF; - $this->assertSame($expected, $modified); - } - - /** - * @group Audit::getModified - * @test - */ - public function itReturnsDecodedAuditableAttributes() - { - $article = new class() extends Article { - protected $table = 'articles'; - - protected $attributeModifiers = [ - 'title' => Base64Encoder::class, - 'content' => LeftRedactor::class, - ]; - }; - - // Audit with redacted/encoded attributes - $audit = AuditFactory::new()->create([ - 'auditable_type' => get_class($article), - 'old_values' => [ - 'title' => 'SG93IFRvIEF1ZGl0IE1vZGVscw==', - 'content' => '##A', - 'reviewed' => 0, - ], - 'new_values' => [ - 'title' => 'SG93IFRvIEF1ZGl0IEVsb3F1ZW50IE1vZGVscw==', - 'content' => '############################################kage.', - 'reviewed' => 1, - ], - ]); - - $this->assertCount(3, $modified = $audit->getModified()); - - Assert::assertArraySubset([ - 'title' => [ - 'new' => 'HOW TO AUDIT ELOQUENT MODELS', - 'old' => 'HOW TO AUDIT MODELS', - ], - 'content' => [ - 'new' => '############################################kage.', - 'old' => '##A', - ], - 'reviewed' => [ - 'new' => true, - 'old' => false, - ], - ], $modified, true); - } - - /** - * @group Audit::getTags - * @test - */ - public function itReturnsTags() - { - $audit = AuditFactory::new()->create([ - 'tags' => 'foo,bar,baz', - ]); - - $this->assertIsArray($audit->getTags()); - Assert::assertArraySubset([ - 'foo', - 'bar', - 'baz', - ], $audit->getTags(), true); - } - - /** - * @group Audit::getTags - * @test - */ - public function itReturnsEmptyTags() - { - $audit = AuditFactory::new()->create([ - 'tags' => null, - ]); - - $this->assertIsArray($audit->getTags()); - $this->assertEmpty($audit->getTags()); - } -} + assertSame($expected, $modified); +}); +test('it returns decoded auditable attributes', function () { + $article = new class () extends Article { + protected $table = 'articles'; + + protected $attributeModifiers = [ + 'title' => Base64Encoder::class, + 'content' => LeftRedactor::class, + ]; + }; + + // Audit with redacted/encoded attributes + /** @var Audit $audit */ + $audit = AuditFactory::new()->create([ + 'auditable_type' => get_class($article), + 'old_values' => [ + 'title' => 'SG93IFRvIEF1ZGl0IE1vZGVscw==', + 'content' => '##A', + 'reviewed' => 0, + ], + 'new_values' => [ + 'title' => 'SG93IFRvIEF1ZGl0IEVsb3F1ZW50IE1vZGVscw==', + 'content' => '############################################kage.', + 'reviewed' => 1, + ], + ]); + + assertCount(3, $modified = $audit->getModified()); + + Assert::assertArraySubset([ + 'title' => [ + 'new' => 'HOW TO AUDIT ELOQUENT MODELS', + 'old' => 'HOW TO AUDIT MODELS', + ], + 'content' => [ + 'new' => '############################################kage.', + 'old' => '##A', + ], + 'reviewed' => [ + 'new' => true, + 'old' => false, + ], + ], $modified, true); +}); + +test('it returns tags', function () { + /** @var Audit $audit */ + $audit = AuditFactory::new()->create([ + 'tags' => 'foo,bar,baz', + ]); + + assertIsArray($audit->getTags()); + Assert::assertArraySubset([ + 'foo', + 'bar', + 'baz', + ], $audit->getTags(), true); +}); + +test('it returns empty tags', function () { + /** @var Audit $audit */ + $audit = AuditFactory::new()->create([ + 'tags' => null, + ]); + + assertIsArray($audit->getTags()); + assertEmpty($audit->getTags()); +}); diff --git a/tests/Unit/AuditableObserverTest.php b/tests/Unit/AuditableObserverTest.php index 7f1b9d9..0082ee7 100644 --- a/tests/Unit/AuditableObserverTest.php +++ b/tests/Unit/AuditableObserverTest.php @@ -3,74 +3,29 @@ namespace Ensi\LaravelAuditing\Tests\Unit; use Ensi\LaravelAuditing\AuditableObserver; -use Ensi\LaravelAuditing\Tests\AuditingTestCase; -use Ensi\LaravelAuditing\Tests\Models\Article; +use Ensi\LaravelAuditing\Tests\Data\Models\Article; +use Ensi\LaravelAuditing\Tests\TestCase; -class AuditableObserverTest extends AuditingTestCase -{ - /** - * @group AuditableObserver::retrieved - * @group AuditableObserver::created - * @group AuditableObserver::updated - * @group AuditableObserver::deleted - * @group AuditableObserver::restoring - * @group AuditableObserver::restored - * @test - * - * @dataProvider auditableObserverTestProvider - * - * @param string $eventMethod - * @param bool $expectedBefore - * @param bool $expectedAfter - */ - public function itExecutesTheAuditorSuccessfully(string $eventMethod, bool $expectedBefore, bool $expectedAfter) - { - $observer = new AuditableObserver(); - $model = Article::factory()->create(); +use function PHPUnit\Framework\assertSame; - $this->assertSame($expectedBefore, $observer::$restoring); +uses(TestCase::class); - $observer->$eventMethod($model); +test('it executes the auditor successfully', function (string $eventMethod, bool $expectedBefore, bool $expectedAfter) { + /** @var TestCase $this */ - $this->assertSame($expectedAfter, $observer::$restoring); - } + $observer = new AuditableObserver(); + $model = Article::factory()->create(); - /** - * @return array - */ - public static function auditableObserverTestProvider(): array - { - return [ - [ - 'retrieved', - false, - false, - ], - [ - 'created', - false, - false, - ], - [ - 'updated', - false, - false, - ], - [ - 'deleted', - false, - false, - ], - [ - 'restoring', - false, - true, - ], - [ - 'restored', - true, - false, - ], - ]; - } -} + assertSame($expectedBefore, $observer::$restoring); + + $observer->$eventMethod($model); + + assertSame($expectedAfter, $observer::$restoring); +})->with([ + ['retrieved', false, false], + ['created', false, false], + ['updated', false, false], + ['deleted', false, false], + ['restoring', false, true], + ['restored', true, false], +]); diff --git a/tests/Unit/AuditableTest.php b/tests/Unit/AuditableTest.php index 8802cd7..b1d4a00 100644 --- a/tests/Unit/AuditableTest.php +++ b/tests/Unit/AuditableTest.php @@ -4,1344 +4,1000 @@ use Carbon\Carbon; use Ensi\LaravelAuditing\Contracts\Auditable; -use Ensi\LaravelAuditing\Database\Factories\AuditFactory; use Ensi\LaravelAuditing\Encoders\Base64Encoder; use Ensi\LaravelAuditing\Exceptions\AuditableTransitionException; use Ensi\LaravelAuditing\Exceptions\AuditingException; use Ensi\LaravelAuditing\Facades\Subject; -use Ensi\LaravelAuditing\Models\Audit; use Ensi\LaravelAuditing\Redactors\LeftRedactor; use Ensi\LaravelAuditing\Redactors\RightRedactor; -use Ensi\LaravelAuditing\Tests\AuditingTestCase; -use Ensi\LaravelAuditing\Tests\Models\ApiModel; -use Ensi\LaravelAuditing\Tests\Models\Article; -use Ensi\LaravelAuditing\Tests\Models\User; -use Ensi\LaravelAuditing\Tests\Models\VirtualUser; +use Ensi\LaravelAuditing\Tests\Data\Models\ApiModel; +use Ensi\LaravelAuditing\Tests\Data\Models\Article; +use Ensi\LaravelAuditing\Tests\Data\Models\Factories\AuditFactory; +use Ensi\LaravelAuditing\Tests\Data\Models\User; +use Ensi\LaravelAuditing\Tests\Data\Models\VirtualUser; +use Ensi\LaravelAuditing\Tests\TestCase; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\App; use Illuminate\Support\Str; use Illuminate\Testing\Assert; -use ReflectionClass; - -class AuditableTest extends AuditingTestCase -{ - private const AUDIT_FIELDS_COUNT = 14; - - /** - * {@inheritdoc} - */ - public function setUp(): void - { - parent::setUp(); - - // Clear morph maps - Relation::morphMap([], false); - } - - /** - * @group Auditable::isAuditingEnabled - * @test - */ - public function itWillNotAuditModelsWhenRunningFromTheConsole() - { - $this->app['config']->set('laravel-auditing.console', false); - - $this->assertFalse(Article::isAuditingEnabled()); - } - - /** - * @group Auditable::isAuditingEnabled - * @test - */ - public function itWillAuditModelsWhenRunningFromTheConsole() - { - $this->app['config']->set('laravel-auditing.console', true); - - $this->assertTrue(Article::isAuditingEnabled()); - } - - /** - * @group Auditable::isAuditingEnabled - * @test - */ - public function itWillAlwaysAuditModelsWhenNotRunningFromTheConsole() - { - App::shouldReceive('runningInConsole') - ->andReturn(false); - - $this->app['config']->set('laravel-auditing.console', false); - - $this->assertTrue(Article::isAuditingEnabled()); - } - - /** - * @group Auditable::bootAuditable - * @test - */ - public function itWillNotBootTraitWhenStaticFlagIsSet() - { - App::spy(); - - Article::$auditingDisabled = true; - - new Article(); - - App::shouldNotHaveReceived('runningInConsole'); - - Article::$auditingDisabled = false; - } - - /** - * @group Auditable::getAuditEvent - * @test - */ - public function itReturnsNullWhenTheAuditEventIsNotSet() - { - $model = new Article(); - - $this->assertNull($model->getAuditEvent()); - } - /** - * @group Auditable::getAuditEvent - * @test - */ - public function itReturnsTheAuditEventThatHasBeenSet() - { - $model = new Article(); - $model->setAuditEvent('created'); +use function PHPUnit\Framework\assertCount; +use function PHPUnit\Framework\assertFalse; +use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertNull; +use function PHPUnit\Framework\assertSame; +use function PHPUnit\Framework\assertTrue; - $this->assertSame('created', $model->getAuditEvent()); - } - - /** - * @group Auditable::getAuditEvents - * @test - */ - public function itReturnsTheDefaultAuditEvents() - { - $model = new Article(); - - Assert::assertArraySubset([ - 'created', - 'updated', - 'deleted', - 'restored', - ], $model->getAuditEvents(), true); - } - - /** - * @group Auditable::getAuditEvents - * @test - */ - public function itReturnsTheCustomAuditEventsFromAttribute() - { - $model = new Article(); - - $model->auditEvents = [ - 'published' => 'getPublishedEventAttributes', - 'archived', - ]; - - Assert::assertArraySubset([ - 'published' => 'getPublishedEventAttributes', - 'archived', - ], $model->getAuditEvents(), true); - } - - /** - * @group Auditable::getAuditEvents - * @test - */ - public function itReturnsTheCustomAuditEventsFromConfig() - { - $this->app['config']->set('laravel-auditing.events', [ - 'published' => 'getPublishedEventAttributes', - 'archived', - ]); - - $model = new Article(); - - Assert::assertArraySubset([ - 'published' => 'getPublishedEventAttributes', - 'archived', - ], $model->getAuditEvents(), true); - } - - /** - * @group Auditable::setAuditEvent - * @group Auditable::readyForAuditing - * @test - */ - public function itIsNotReadyForAuditingWithCustomEvent() - { - $model = new Article(); - - $model->setAuditEvent('published'); - $this->assertFalse($model->readyForAuditing()); - } +use ReflectionClass; - /** - * @group Auditable::setAuditEvent - * @group Auditable::readyForAuditing - * @test - */ - public function itIsReadyForAuditingWithCustomEvents() - { - $model = new Article(); +uses(TestCase::class); - $model->auditEvents = [ - 'published' => 'getPublishedEventAttributes', - '*ted' => 'getMultiEventAttributes', - 'archived', - ]; +beforeEach(function () { + // Clear morph maps + Relation::morphMap([], false); +}); - $model->setAuditEvent('published'); - $this->assertTrue($model->readyForAuditing()); +test('it will not audit models when running from the console', function () { + config()->set('laravel-auditing.console', false); - $model->setAuditEvent('archived'); - $this->assertTrue($model->readyForAuditing()); + assertFalse(Article::isAuditingEnabled()); +}); - $model->setAuditEvent('redacted'); - $this->assertTrue($model->readyForAuditing()); - } +test('it will audit models when running from the console', function () { + config()->set('laravel-auditing.console', true); - /** - * @group Auditable::setAuditEvent - * @group Auditable::readyForAuditing - * @test - */ - public function itIsReadyForAuditingWithRegularEvents() - { - $model = new Article(); + assertTrue(Article::isAuditingEnabled()); +}); - $model->setAuditEvent('created'); - $this->assertTrue($model->readyForAuditing()); +test('it will always audit models when not running from the console', function () { + App::shouldReceive('runningInConsole') + ->andReturn(false); - $model->setAuditEvent('updated'); - $this->assertTrue($model->readyForAuditing()); + config()->set('laravel-auditing.console', false); - $model->setAuditEvent('deleted'); - $this->assertTrue($model->readyForAuditing()); + assertTrue(Article::isAuditingEnabled()); +}); - $model->setAuditEvent('restored'); - $this->assertTrue($model->readyForAuditing()); - } +test('it will not boot trait when static flag is set', function () { + App::spy(); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itFailsWhenAnInvalidAuditEventIsSet() - { - $this->expectException(AuditingException::class); - $this->expectExceptionMessage('A valid audit event has not been set'); + Article::$auditingDisabled = true; - $model = new Article(); + new Article(); - $model->setAuditEvent('published'); + App::shouldNotHaveReceived('runningInConsole'); - $model->toAudit(); - } + Article::$auditingDisabled = false; +}); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - * - * @dataProvider auditCustomAttributeGetterFailTestProvider - * - * @param string $event - * @param array $auditEvents - * @param string $exceptionMessage - */ - public function itFailsWhenTheCustomAttributeGettersAreMissing( - string $event, - array $auditEvents, - string $exceptionMessage - ) { - $this->expectException(AuditingException::class); - $this->expectExceptionMessage($exceptionMessage); - - $model = new Article(); - - $model->auditEvents = $auditEvents; - - $model->setAuditEvent($event); - - $model->toAudit(); - } +test('it returns null when the audit event is not set', function () { + $model = new Article(); - /** - * @return array - */ - public static function auditCustomAttributeGetterFailTestProvider(): array - { - return [ - [ - 'published', - [ - 'published' => 'getPublishedEventAttributes', - ], - 'Unable to handle "published" event, getPublishedEventAttributes() method missing', - ], - [ - 'archived', - [ - 'archived', - ], - 'Unable to handle "archived" event, getArchivedEventAttributes() method missing', - ], - [ - 'redacted', - [ - '*ed', - ], - 'Unable to handle "redacted" event, getRedactedEventAttributes() method missing', - ], - [ - 'redacted', - [ - '*ed' => 'getMultiEventAttributes', - ], - 'Unable to handle "redacted" event, getMultiEventAttributes() method missing', - ], - ]; - } + assertNull($model->getAuditEvent()); +}); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itFailsWhenTheIpAddressResolverImplementationIsInvalid() - { - $this->expectException(AuditingException::class); - $this->expectExceptionMessage('Invalid IpAddressResolver implementation'); +test('it returns the audit event that has been set', function () { + $model = new Article(); + $model->setAuditEvent('created'); - $this->app['config']->set('laravel-auditing.resolver.ip_address', null); + assertSame('created', $model->getAuditEvent()); +}); - $model = new Article(); +test('it returns the default audit events', function () { + $model = new Article(); - $model->setAuditEvent('created'); + Assert::assertArraySubset([ + 'created', + 'updated', + 'deleted', + 'restored', + ], $model->getAuditEvents(), true); +}); - $model->toAudit(); - } +test('it returns the custom audit events from attribute', function () { + $model = new Article(); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itFailsWhenTheUrlResolverImplementationIsInvalid() - { - $this->expectException(AuditingException::class); - $this->expectExceptionMessage('Invalid UrlResolver implementation'); + $model->auditEvents = [ + 'published' => 'getPublishedEventAttributes', + 'archived', + ]; - $this->app['config']->set('laravel-auditing.resolver.url', null); + Assert::assertArraySubset([ + 'published' => 'getPublishedEventAttributes', + 'archived', + ], $model->getAuditEvents(), true); +}); - $model = new Article(); +test('it returns the custom audit events from config', function () { + config()->set('laravel-auditing.events', [ + 'published' => 'getPublishedEventAttributes', + 'archived', + ]); - $model->setAuditEvent('created'); + $model = new Article(); - $model->toAudit(); - } + Assert::assertArraySubset([ + 'published' => 'getPublishedEventAttributes', + 'archived', + ], $model->getAuditEvents(), true); +}); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itFailsWhenTheUserAgentResolverImplementationIsInvalid() - { - $this->expectException(AuditingException::class); - $this->expectExceptionMessage('Invalid UserAgentResolver implementation'); +test('it is not ready for auditing with custom event', function () { + $model = new Article(); - $this->app['config']->set('laravel-auditing.resolver.user_agent', null); + $model->setAuditEvent('published'); + assertFalse($model->readyForAuditing()); +}); - $model = new Article(); +test('it is ready for auditing with custom events', function () { + $model = new Article(); - $model->setAuditEvent('created'); + $model->auditEvents = [ + 'published' => 'getPublishedEventAttributes', + '*ted' => 'getMultiEventAttributes', + 'archived', + ]; - $model->toAudit(); - } + $model->setAuditEvent('published'); + assertTrue($model->readyForAuditing()); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itFailsWhenTheUserResolverImplementationIsInvalid() - { - $this->expectException(AuditingException::class); - $this->expectExceptionMessage('Invalid UserResolver implementation'); + $model->setAuditEvent('archived'); + assertTrue($model->readyForAuditing()); - $this->app['config']->set('laravel-auditing.resolver.user', null); + $model->setAuditEvent('redacted'); + assertTrue($model->readyForAuditing()); +}); - $model = new Article(); +test('it is ready for auditing with regular events', function () { + $model = new Article(); - $model->setAuditEvent('created'); + $model->setAuditEvent('created'); + assertTrue($model->readyForAuditing()); - $model->toAudit(); - } + $model->setAuditEvent('updated'); + assertTrue($model->readyForAuditing()); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itReturnsTheAuditData() - { - $now = Carbon::now(); - - $model = Article::factory()->make([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ]); + $model->setAuditEvent('deleted'); + assertTrue($model->readyForAuditing()); - $model->setAuditEvent('created'); + $model->setAuditEvent('restored'); + assertTrue($model->readyForAuditing()); +}); - $this->assertCount(self::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); +test('it fails when an invalid audit event is set', function () { + $model = new Article(); - Assert::assertArraySubset([ - 'old_values' => [], - 'new_values' => [ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now->toDateTimeString(), - ], - 'event' => 'created', - 'auditable_id' => null, - 'auditable_type' => Article::class, - 'subject_id' => null, - 'subject_type' => null, - 'url' => 'console', - 'ip_address' => '127.0.0.1', - 'user_agent' => 'Symfony', - 'tags' => null, - ], $auditData, true); - } + $model->setAuditEvent('published'); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itReturnsTheAuditDataIncludingSubjectAttributes() - { - $user = User::factory()->create(); - Subject::attach($user); - $now = Carbon::now(); - - $model = Article::factory()->make([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ]); + $model->toAudit(); +})->expectExceptionObject(new AuditingException('A valid audit event has not been set')); - $model->setAuditEvent('created'); +test('it fails when the custom attribute getters are missing', function ( + string $event, + array $auditEvents, + string $exceptionMessage +) { + $this->expectException(AuditingException::class); + $this->expectExceptionMessage($exceptionMessage); - $this->assertCount(self::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + $model = new Article(); - Assert::assertArraySubset([ - 'old_values' => [], - 'new_values' => [ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now->toDateTimeString(), - ], - 'event' => 'created', - 'auditable_id' => null, - 'auditable_type' => Article::class, - 'subject_id' => $user->getKey(), - 'subject_type' => User::class, - 'url' => 'console', - 'ip_address' => '127.0.0.1', - 'user_agent' => 'Symfony', - 'user_id' => null, - 'tags' => null, - ], $auditData, true); - } + $model->auditEvents = $auditEvents; - /** - * @dataProvider userResolverProvider - * @test - */ - public function itReturnsTheAuditDataIncludingUserId( - string $guard, - string $driver, - ?string $id - ) { - $this->app['config']->set('laravel-auditing.user.guards', [$guard]); - - $user = User::factory()->create(); - $this->actingAs($user, $driver); - $now = Carbon::now(); - - $model = Article::factory()->make([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ]); + $model->setAuditEvent($event); - $model->setAuditEvent('created'); - - $this->assertCount(self::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); - - Assert::assertArraySubset([ - 'old_values' => [], - 'new_values' => [ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now->toDateTimeString(), - ], - 'event' => 'created', - 'auditable_id' => null, - 'auditable_type' => Article::class, - 'subject_id' => null, - 'subject_type' => null, - 'url' => 'console', - 'ip_address' => '127.0.0.1', - 'user_agent' => 'Symfony', - 'user_id' => $id, - 'tags' => null, - ], $auditData, true); - } - - public static function userResolverProvider(): array - { - return [ - ['api', 'web', null], - ['web', 'api', null], - ['api', 'api', '1'], - ['web', 'web', '1'], + $model->toAudit(); +})->with([ + [ + 'published', + [ + 'published' => 'getPublishedEventAttributes', + ], + 'Unable to handle "published" event, getPublishedEventAttributes() method missing', + ], + [ + 'archived', + [ + 'archived', + ], + 'Unable to handle "archived" event, getArchivedEventAttributes() method missing', + ], + [ + 'redacted', + [ + '*ed', + ], + 'Unable to handle "redacted" event, getRedactedEventAttributes() method missing', + ], + [ + 'redacted', + [ + '*ed' => 'getMultiEventAttributes', + ], + 'Unable to handle "redacted" event, getMultiEventAttributes() method missing', + ], +]); + +test('it fails when the ip address resolver implementation is invalid', function () { + config()->set('laravel-auditing.resolver.ip_address', null); + + $model = new Article(); + + $model->setAuditEvent('created'); + + $model->toAudit(); +})->expectExceptionObject(new AuditingException('Invalid IpAddressResolver implementation')); + +test('it fails when the url resolver implementation is invalid', function () { + config()->set('laravel-auditing.resolver.url', null); + + $model = new Article(); + + $model->setAuditEvent('created'); + + $model->toAudit(); +})->expectExceptionObject(new AuditingException('Invalid UrlResolver implementation')); + +test('it fails when the user agent resolver implementation is invalid', function () { + config()->set('laravel-auditing.resolver.user_agent', null); + + $model = new Article(); + + $model->setAuditEvent('created'); + + $model->toAudit(); +})->expectExceptionObject(new AuditingException('Invalid UserAgentResolver implementation')); + +test('it fails when the user resolver implementation is invalid', function () { + config()->set('laravel-auditing.resolver.user', null); + + $model = new Article(); + + $model->setAuditEvent('created'); + + $model->toAudit(); +})->expectExceptionObject(new AuditingException('Invalid UserResolver implementation')); + +test('it returns the audit data', function () { + $now = Carbon::now(); + + /** @var Article $model */ + $model = Article::factory()->make([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ]); + + $model->setAuditEvent('created'); + + assertCount(Article::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + + Assert::assertArraySubset([ + 'old_values' => [], + 'new_values' => [ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now->toDateTimeString(), + ], + 'event' => 'created', + 'auditable_id' => null, + 'auditable_type' => Article::class, + 'subject_id' => null, + 'subject_type' => null, + 'url' => 'console', + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Symfony', + 'tags' => null, + ], $auditData, true); +}); + +test('it returns the audit data including subject attributes', function () { + $user = User::factory()->create(); + Subject::attach($user); + $now = Carbon::now(); + + /** @var Article $model */ + $model = Article::factory()->make([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ]); + + $model->setAuditEvent('created'); + + assertCount(Article::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + + Assert::assertArraySubset([ + 'old_values' => [], + 'new_values' => [ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now->toDateTimeString(), + ], + 'event' => 'created', + 'auditable_id' => null, + 'auditable_type' => Article::class, + 'subject_id' => $user->getKey(), + 'subject_type' => User::class, + 'url' => 'console', + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Symfony', + 'user_id' => null, + 'tags' => null, + ], $auditData, true); +}); + +test('it returns the audit data including user id', function ( + string $guard, + string $driver, + ?string $id +) { + config()->set('laravel-auditing.user.guards', [$guard]); + + $user = User::factory()->create(); + $this->actingAs($user, $driver); + $now = Carbon::now(); + + /** @var Article $model */ + $model = Article::factory()->make([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ]); + + $model->setAuditEvent('created'); + + assertCount(Article::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + + Assert::assertArraySubset([ + 'old_values' => [], + 'new_values' => [ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now->toDateTimeString(), + ], + 'event' => 'created', + 'auditable_id' => null, + 'auditable_type' => Article::class, + 'subject_id' => null, + 'subject_type' => null, + 'url' => 'console', + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Symfony', + 'user_id' => $id, + 'tags' => null, + ], $auditData, true); +})->with([ + ['api', 'web', null], + ['web', 'api', null], + ['api', 'api', '1'], + ['web', 'web', '1'], +]); + +test('it returns the audit data including virtual user id', function () { + $this->actingAs(new VirtualUser(), 'api'); + + $now = Carbon::now(); + + /** @var Article $model */ + $model = Article::factory()->make([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ]); + + $model->setAuditEvent('created'); + + assertCount(Article::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + + Assert::assertArraySubset([ + 'old_values' => [], + 'new_values' => [ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now->toDateTimeString(), + ], + 'event' => 'created', + 'auditable_id' => null, + 'auditable_type' => Article::class, + 'subject_id' => null, + 'subject_type' => null, + 'url' => 'console', + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Symfony', + 'user_id' => VirtualUser::ID, + 'tags' => null, + ], $auditData, true); +}); + +test('it returns the audit data including extra', function () { + $now = Carbon::now(); + + /** @var Article $model */ + $model = Article::factory()->make([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => $now, + ]); + + $model->setAuditEvent('created'); + + assertCount(Article::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + + Assert::assertArraySubset([ + 'extra' => [ + 'year' => $now->year, + ], + 'event' => 'created', + 'auditable_id' => null, + 'auditable_type' => Article::class, + ], $auditData, true); +}); + +test('it returns the default extra', function () { + /** @var Article $model */ + $model = Article::factory()->make(); + $model->setAuditEvent('created'); + + Assert::assertArraySubset([ + 'extra' => null, + ], $model->toAudit(), true); +}); + +test('it excludes attributes from the audit data when in strict mode', function () { + config()->set('laravel-auditing.strict', true); + + $model = Article::factory()->make([ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => Carbon::now(), + ]); + + $model->setHidden([ + 'reviewed', + ]); + + $model->setVisible([ + 'title', + 'content', + ]); + + $model->setAuditEvent('created'); + + assertCount(Article::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + + Assert::assertArraySubset([ + 'old_values' => [], + 'new_values' => [ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + ], + 'event' => 'created', + 'auditable_id' => null, + 'auditable_type' => Article::class, + 'subject_id' => null, + 'subject_type' => null, + 'url' => 'console', + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Symfony', + 'tags' => null, + ], $auditData, true); +}); + +test('it fails when the attribute modifier implementation is invalid', function () { + /** @var Article $model */ + $model = Article::factory()->make(); + + $model->attributeModifiers = [ + 'title' => 'invalidAttributeRedactorOrEncoder', + ]; + + $model->setAuditEvent('created'); + + $model->toAudit(); +})->expectExceptionObject(new AuditingException('Invalid AttributeModifier implementation: invalidAttributeRedactorOrEncoder')); + +test('it modifies the audit attributes successfully', function () { + /** @var Article $model */ + $model = Article::factory()->make([ + 'title' => 'How To Audit Models', + 'content' => 'N/A', + 'reviewed' => 0, + 'published_at' => null, + ]); + + $now = Carbon::now(); + + $model->syncOriginal(); + + $model->title = 'How To Audit Eloquent Models'; + $model->content = 'First step: install the laravel-auditing package.'; + $model->reviewed = 1; + $model->published_at = $now; + + $model->setAuditEvent('updated'); + + $model->attributeModifiers = [ + 'title' => RightRedactor::class, + 'content' => LeftRedactor::class, + 'reviewed' => Base64Encoder::class, + ]; + + Assert::assertArraySubset([ + 'old_values' => [ + 'title' => 'Ho#################', + 'content' => '##A', + 'published_at' => null, + 'reviewed' => 'MA==', + ], + 'new_values' => [ + 'title' => 'How#########################', + 'content' => '############################################kage.', + 'published_at' => $now->toDateTimeString(), + 'reviewed' => 'MQ==', + ], + ], $model->toAudit(), true); +}); + +test('it transforms the audit data', function () { + $model = new class () extends Article { + protected $attributes = [ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => '2012-06-14 15:03:00', ]; - } - /** - * @test - */ - public function itReturnsTheAuditDataIncludingVirtualUserId() - { - $this->actingAs(new VirtualUser(), 'api'); + public function transformAudit(array $data): array + { + $data['new_values']['slug'] = Str::slug($data['new_values']['title']); - $now = Carbon::now(); + return $data; + } + }; - $model = Article::factory()->make([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ]); + $model->setAuditEvent('created'); - $model->setAuditEvent('created'); + assertCount(Article::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); - $this->assertCount(self::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + Assert::assertArraySubset([ + 'new_values' => [ + 'title' => 'How To Audit Eloquent Models', + 'content' => 'First step: install the laravel-auditing package.', + 'reviewed' => 1, + 'published_at' => '2012-06-14 15:03:00', + 'slug' => 'how-to-audit-eloquent-models', + ], + ], $auditData, true); +}); - Assert::assertArraySubset([ - 'old_values' => [], - 'new_values' => [ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now->toDateTimeString(), - ], - 'event' => 'created', - 'auditable_id' => null, - 'auditable_type' => Article::class, - 'subject_id' => null, - 'subject_type' => null, - 'url' => 'console', - 'ip_address' => '127.0.0.1', - 'user_agent' => 'Symfony', - 'user_id' => VirtualUser::ID, - 'tags' => null, - ], $auditData, true); - } +test('it returns the default attributes to be included in the audit', function () { + $model = new Article(); - /** - * @test - */ - public function itReturnsTheAuditDataIncludingExtra() - { - $now = Carbon::now(); - - $model = Article::factory()->make([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => $now, - ]); + Assert::assertArraySubset([], $model->getAuditInclude(), true); +}); - $model->setAuditEvent('created'); +test('it returns the custom attributes to be included in the audit', function () { + $model = new Article(); - $this->assertCount(self::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + $model->auditInclude = [ + 'title', + 'content', + ]; - Assert::assertArraySubset([ - 'extra' => [ - 'year' => $now->year, - ], - 'event' => 'created', - 'auditable_id' => null, - 'auditable_type' => Article::class, - ], $auditData, true); - } + Assert::assertArraySubset([ + 'title', + 'content', + ], $model->getAuditInclude(), true); +}); - /** - * @test - */ - public function itReturnsTheDefaultExtra() - { - $model = Article::factory()->make(); - $model->setAuditEvent('created'); +test('it returns the default attributes to be excluded from the audit', function () { + $model = new Article(); - Assert::assertArraySubset([ - 'extra' => null, - ], $model->toAudit(), true); - } + Assert::assertArraySubset([], $model->getAuditExclude(), true); +}); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itExcludesAttributesFromTheAuditDataWhenInStrictMode() - { - $this->app['config']->set('laravel-auditing.strict', true); - - $model = Article::factory()->make([ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => Carbon::now(), - ]); +test('it returns the custom attributes to be excluded from the audit', function () { + $model = new Article(); - $model->setHidden([ - 'reviewed', - ]); + $model->auditExclude = [ + 'published_at', + ]; - $model->setVisible([ - 'title', - 'content', - ]); + Assert::assertArraySubset([ + 'published_at', + ], $model->getAuditExclude(), true); +}); - $model->setAuditEvent('created'); +test('it returns the default audit strict value', function () { + $model = new Article(); - $this->assertCount(self::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); + assertFalse($model->getAuditStrict()); +}); - Assert::assertArraySubset([ - 'old_values' => [], - 'new_values' => [ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - ], - 'event' => 'created', - 'auditable_id' => null, - 'auditable_type' => Article::class, - 'subject_id' => null, - 'subject_type' => null, - 'url' => 'console', - 'ip_address' => '127.0.0.1', - 'user_agent' => 'Symfony', - 'tags' => null, - ], $auditData, true); - } +test('it returns the custom audit strict value from attribute', function () { + $model = new Article(); - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itFailsWhenTheAttributeModifierImplementationIsInvalid() - { - $this->expectException(AuditingException::class); - $this->expectExceptionMessage('Invalid AttributeModifier implementation: invalidAttributeRedactorOrEncoder'); + $model->auditStrict = true; - $model = Article::factory()->make(); + assertTrue($model->getAuditStrict()); +}); - $model->attributeModifiers = [ - 'title' => 'invalidAttributeRedactorOrEncoder', - ]; - - $model->setAuditEvent('created'); - - $model->toAudit(); - } - - /** - * @group Auditable::setAuditEvent - * @group Auditable::toAudit - * @test - */ - public function itModifiesTheAuditAttributesSuccessfully() - { - $model = Article::factory()->make([ - 'title' => 'How To Audit Models', - 'content' => 'N/A', - 'reviewed' => 0, - 'published_at' => null, - ]); +test('it returns the custom audit strict value from config', function () { + config()->set('laravel-auditing.strict', true); - $now = Carbon::now(); + $model = new Article(); - $model->syncOriginal(); + assertTrue($model->getAuditStrict()); +}); - $model->title = 'How To Audit Eloquent Models'; - $model->content = 'First step: install the laravel-auditing package.'; - $model->reviewed = 1; - $model->published_at = $now; +test('it returns the default audit timestamps value', function () { + $model = new Article(); - $model->setAuditEvent('updated'); + assertFalse($model->getAuditTimestamps()); +}); - $model->attributeModifiers = [ - 'title' => RightRedactor::class, - 'content' => LeftRedactor::class, - 'reviewed' => Base64Encoder::class, - ]; +test('it returns the custom audit timestamps value from attribute', function () { + $model = new Article(); - Assert::assertArraySubset([ - 'old_values' => [ - 'title' => 'Ho#################', - 'content' => '##A', - 'published_at' => null, - 'reviewed' => 'MA==', - ], - 'new_values' => [ - 'title' => 'How#########################', - 'content' => '############################################kage.', - 'published_at' => $now->toDateTimeString(), - 'reviewed' => 'MQ==', - ], - ], $model->toAudit(), true); - } + $model->auditTimestamps = true; - /** - * @group Auditable::setAuditEvent - * @group Auditable::transformAudit - * @group Auditable::toAudit - * @test - */ - public function itTransformsTheAuditData() - { - $model = new class() extends Article { - protected $attributes = [ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => '2012-06-14 15:03:00', - ]; + assertTrue($model->getAuditTimestamps()); +}); - public function transformAudit(array $data): array - { - $data['new_values']['slug'] = Str::slug($data['new_values']['title']); +test('it returns the custom audit timestamps value from config', function () { + config()->set('laravel-auditing.timestamps', true); - return $data; - } - }; + $model = new Article(); - $model->setAuditEvent('created'); + assertTrue($model->getAuditTimestamps()); +}); - $this->assertCount(self::AUDIT_FIELDS_COUNT, $auditData = $model->toAudit()); +test('it returns the default audit driver value', function () { + $model = new Article(); - Assert::assertArraySubset([ - 'new_values' => [ - 'title' => 'How To Audit Eloquent Models', - 'content' => 'First step: install the laravel-auditing package.', - 'reviewed' => 1, - 'published_at' => '2012-06-14 15:03:00', - 'slug' => 'how-to-audit-eloquent-models', - ], - ], $auditData, true); - } + assertSame('database', $model->getAuditDriver()); +}); - /** - * @group Auditable::getAuditInclude - * @test - */ - public function itReturnsTheDefaultAttributesToBeIncludedInTheAudit() - { - $model = new Article(); +test('it returns the custom audit driver value from attribute', function () { + $model = new Article(); - Assert::assertArraySubset([], $model->getAuditInclude(), true); - } + $model->auditDriver = 'RedisDriver'; - /** - * @group Auditable::getAuditInclude - * @test - */ - public function itReturnsTheCustomAttributesToBeIncludedInTheAudit() - { - $model = new Article(); - - $model->auditInclude = [ - 'title', - 'content', - ]; + assertSame('RedisDriver', $model->getAuditDriver()); +}); - Assert::assertArraySubset([ - 'title', - 'content', - ], $model->getAuditInclude(), true); - } +test('it returns the custom audit driver value from config', function () { + config()->set('laravel-auditing.driver', 'RedisDriver'); - /** - * @group Auditable::getAuditExclude - * @test - */ - public function itReturnsTheDefaultAttributesToBeExcludedFromTheAudit() - { - $model = new Article(); + $model = new Article(); - Assert::assertArraySubset([], $model->getAuditExclude(), true); - } + assertSame('RedisDriver', $model->getAuditDriver()); +}); - /** - * @group Auditable::getAuditExclude - * @test - */ - public function itReturnsTheCustomAttributesToBeExcludedFromTheAudit() - { - $model = new Article(); +test('it returns the default audit threshold value', function () { + $model = new Article(); - $model->auditExclude = [ - 'published_at', - ]; + assertSame(0, $model->getAuditThreshold()); +}); - Assert::assertArraySubset([ - 'published_at', - ], $model->getAuditExclude(), true); - } +test('it returns the custom audit threshold value from attribute', function () { + $model = new Article(); - /** - * @group Auditable::getAuditStrict - * @test - */ - public function itReturnsTheDefaultAuditStrictValue() - { - $model = new Article(); + $model->auditThreshold = 10; - $this->assertFalse($model->getAuditStrict()); - } + assertSame(10, $model->getAuditThreshold()); +}); - /** - * @group Auditable::getAuditStrict - * @test - */ - public function itReturnsTheCustomAuditStrictValueFromAttribute() - { - $model = new Article(); +test('it returns the custom audit threshold value from config', function () { + config()->set('laravel-auditing.threshold', 200); - $model->auditStrict = true; + $model = new Article(); - $this->assertTrue($model->getAuditStrict()); - } + assertSame(200, $model->getAuditThreshold()); +}); - /** - * @group Auditable::getAuditStrict - * @test - */ - public function itReturnsTheCustomAuditStrictValueFromConfig() - { - $this->app['config']->set('laravel-auditing.strict', true); +test('it returns the default generated audit tags', function () { + $model = new Article(); - $model = new Article(); + Assert::assertArraySubset([], $model->generateTags(), true); +}); - $this->assertTrue($model->getAuditStrict()); - } - - /** - * @group Auditable::getAuditTimestamps - * @test - */ - public function itReturnsTheDefaultAuditTimestampsValue() - { - $model = new Article(); +test('it returns the custom generated audit tags', function () { + $model = new class () extends Article { + public function generateTags(): array + { + return [ + 'foo', + 'bar', + ]; + } + }; - $this->assertFalse($model->getAuditTimestamps()); - } + Assert::assertArraySubset([ + 'foo', + 'bar', + ], $model->generateTags(), true); +}); - /** - * @group Auditable::getAuditTimestamps - * @test - */ - public function itReturnsTheCustomAuditTimestampsValueFromAttribute() - { - $model = new Article(); +test('it fails to transition when the audit auditable type does not match the model type', function () { - $model->auditTimestamps = true; + $audit = AuditFactory::new()->make([ + 'auditable_type' => User::class, + ]); - $this->assertTrue($model->getAuditTimestamps()); - } + $model = new Article(); + + $model->transitionTo($audit); +})->expectExceptionObject(new AuditableTransitionException('Expected Auditable type Ensi\LaravelAuditing\Tests\Data\Models\Article, got Ensi\LaravelAuditing\Tests\Data\Models\User instead')); - /** - * @group Auditable::getAuditTimestamps - * @test - */ - public function itReturnsTheCustomAuditTimestampsValueFromConfig() - { - $this->app['config']->set('laravel-auditing.timestamps', true); +test('it fails to transition when the audit auditable type does not match the morph map value', function () { + Relation::morphMap([ + 'articles' => Article::class, + ]); - $model = new Article(); + $audit = AuditFactory::new()->make([ + 'auditable_type' => 'users', + ]); - $this->assertTrue($model->getAuditTimestamps()); - } + $model = new Article(); - /** - * @group Auditable::getAuditDriver - * @test - */ - public function itReturnsTheDefaultAuditDriverValue() - { - $model = new Article(); + $model->transitionTo($audit); +})->expectExceptionObject(new AuditableTransitionException('Expected Auditable type articles, got users instead')); - $this->assertSame('database', $model->getAuditDriver()); - } +test('it fails to transition when the audit auditable id does not match the model id', function () { + $firstAudit = Article::factory()->create()->audits()->first(); + $secondModel = Article::factory()->create(); - /** - * @group Auditable::getAuditDriver - * @test - */ - public function itReturnsTheCustomAuditDriverValueFromAttribute() - { - $model = new Article(); + $secondModel->transitionTo($firstAudit); +})->expectExceptionObject(new AuditableTransitionException('Expected Auditable id 2, got 1 instead')); - $model->auditDriver = 'RedisDriver'; +test('it fails to transition when the audit auditable id type does not match the model id type', function () { + $model = Article::factory()->create(); - $this->assertSame('RedisDriver', $model->getAuditDriver()); - } + $audit = AuditFactory::new()->create([ + 'auditable_type' => Article::class, + 'auditable_id' => (string)$model->id, + ]); - /** - * @group Auditable::getAuditDriver - * @test - */ - public function itReturnsTheCustomAuditDriverValueFromConfig() - { - $this->app['config']->set('laravel-auditing.driver', 'RedisDriver'); + // Make sure the auditable_id isn't being cast + $auditReflection = new ReflectionClass($audit); - $model = new Article(); + $auditCastsProperty = $auditReflection->getProperty('casts'); + $auditCastsProperty->setAccessible(true); + $auditCastsProperty->setValue($audit, [ + 'old_values' => 'json', + 'new_values' => 'json', + ]); - $this->assertSame('RedisDriver', $model->getAuditDriver()); - } + $model->transitionTo($audit); +})->expectExceptionObject(new AuditableTransitionException('Expected Auditable id 1, got 1 instead')); - /** - * @group Auditable::getAuditThreshold - * @test - */ - public function itReturnsTheDefaultAuditThresholdValue() - { - $model = new Article(); +test('it transitions when the audit auditable id type does not match the model id type', function () { + /** @var Article $model */ + $model = Article::factory()->create(); - $this->assertSame(0, $model->getAuditThreshold()); + // Key depends on type + if ($model->getKeyType() == 'string') { + $key = (string)$model->id; + } else { + $key = (int)$model->id; } - /** - * @group Auditable::getAuditThreshold - * @test - */ - public function itReturnsTheCustomAuditThresholdValueFromAttribute() - { - $model = new Article(); + $audit = AuditFactory::new()->create([ + 'auditable_type' => Article::class, + 'auditable_id' => $key, + ]); - $model->auditThreshold = 10; + assertInstanceOf(Auditable::class, $model->transitionTo($audit)); +}); - $this->assertSame(10, $model->getAuditThreshold()); - } +test('it fails to transition when an attribute redactor is set', function () { + $model = Article::factory()->create(); - /** - * @group Auditable::getAuditThreshold - * @test - */ - public function itReturnsTheCustomAuditThresholdValueFromConfig() - { - $this->app['config']->set('laravel-auditing.threshold', 200); + $model->attributeModifiers = [ + 'title' => RightRedactor::class, + ]; - $model = new Article(); + $audit = AuditFactory::new()->create([ + 'auditable_id' => $model->getKey(), + 'auditable_type' => Article::class, + ]); - $this->assertSame(200, $model->getAuditThreshold()); - } + $model->transitionTo($audit); +})->expectExceptionObject(new AuditableTransitionException('Cannot transition states when an AttributeRedactor is set')); - /** - * @group Auditable::generateTags - * @test - */ - public function itReturnsTheDefaultGeneratedAuditTags() - { - $model = new Article(); +test('it fails to transition when the auditable attribute compatibility is not met', function () { + $model = Article::factory()->create(); - Assert::assertArraySubset([], $model->generateTags(), true); - } + $incompatibleAudit = AuditFactory::new()->create([ + 'event' => 'created', + 'auditable_id' => $model->getKey(), + 'auditable_type' => Article::class, + 'old_values' => [], + 'new_values' => [ + 'subject' => 'Culpa qui rerum excepturi quisquam quia officiis.', + 'text' => 'Magnam enim suscipit officiis tempore ut quis harum.', + ], + ]); - /** - * @group Auditable::generateTags - * @test - */ - public function itReturnsTheCustomGeneratedAuditTags() - { - $model = new class() extends Article { - public function generateTags(): array - { - return [ - 'foo', - 'bar', - ]; - } - }; + try { + $model->transitionTo($incompatibleAudit); + } catch (AuditableTransitionException $e) { + assertSame( + 'Incompatibility between [Ensi\LaravelAuditing\Tests\Data\Models\Article:1] and [Ensi\LaravelAuditing\Models\Audit:3]', + $e->getMessage() + ); Assert::assertArraySubset([ - 'foo', - 'bar', - ], $model->generateTags(), true); - } - - /** - * @group Auditable::transitionTo - * @test - */ - public function itFailsToTransitionWhenTheAuditAuditableTypeDoesNotMatchTheModelType() - { - $this->expectException(AuditableTransitionException::class); - $this->expectExceptionMessage('Expected Auditable type Ensi\LaravelAuditing\Tests\Models\Article, got Ensi\LaravelAuditing\Tests\Models\User instead'); - - $audit = AuditFactory::new()->make([ - 'auditable_type' => User::class, - ]); - - $model = new Article(); - - $model->transitionTo($audit); - } - - /** - * @group Auditable::transitionTo - * @test - */ - public function itFailsToTransitionWhenTheAuditAuditableTypeDoesNotMatchTheMorphMapValue() - { - $this->expectException(AuditableTransitionException::class); - $this->expectExceptionMessage('Expected Auditable type articles, got users instead'); - + 'subject', + 'text', + ], $e->getIncompatibilities(), true); + } +}); + +test('it transitions to another model state', function ( + bool $morphMap, + array $oldValues, + array $newValues, + array $oldExpectation, + array $newExpectation +) { + $models = Article::factory()->count(2)->create([ + 'title' => 'Facilis voluptas qui impedit deserunt vitae quidem.', + 'content' => 'Consectetur distinctio nihil eveniet cum. Expedita dolores animi dolorum eos repellat rerum.', + ]); + + if ($morphMap) { Relation::morphMap([ 'articles' => Article::class, ]); - - $audit = AuditFactory::new()->make([ - 'auditable_type' => 'users', - ]); - - $model = new Article(); - - $model->transitionTo($audit); - } - - /** - * @group Auditable::transitionTo - * @test - */ - public function itFailsToTransitionWhenTheAuditAuditableIdDoesNotMatchTheModelId() - { - $this->expectException(AuditableTransitionException::class); - $this->expectExceptionMessage('Expected Auditable id 2, got 1 instead'); - - $firstAudit = Article::factory()->create()->audits()->first(); - $secondModel = Article::factory()->create(); - - $secondModel->transitionTo($firstAudit); - } - - /** - * @group Auditable::transitionTo - * @test - */ - public function itFailsToTransitionWhenTheAuditAuditableIdTypeDoesNotMatchTheModelIdType() - { - $this->expectException(AuditableTransitionException::class); - $this->expectExceptionMessage('Expected Auditable id 1, got 1 instead'); - - $model = Article::factory()->create(); - - $audit = AuditFactory::new()->create([ - 'auditable_type' => Article::class, - 'auditable_id' => (string) $model->id, - ]); - - // Make sure the auditable_id isn't being cast - $auditReflection = new ReflectionClass($audit); - - $auditCastsProperty = $auditReflection->getProperty('casts'); - $auditCastsProperty->setAccessible(true); - $auditCastsProperty->setValue($audit, [ - 'old_values' => 'json', - 'new_values' => 'json', - ]); - - $model->transitionTo($audit); - } - - /** - * @group Auditable::transitionTo - * @test - */ - public function itTransitionsWhenTheAuditAuditableIdTypeDoesNotMatchTheModelIdType() - { - $model = Article::factory()->create(); - - // Key depends on type - if ($model->getKeyType() == 'string') { - $key = (string) $model->id; - } else { - $key = (int) $model->id; - } - - $audit = AuditFactory::new()->create([ - 'auditable_type' => Article::class, - 'auditable_id' => $key, - ]); - - $this->assertInstanceOf(Auditable::class, $model->transitionTo($audit)); } - /** - * @group Auditable::transitionTo - * @test - */ - public function itFailsToTransitionWhenAnAttributeRedactorIsSet() - { - $this->expectException(AuditableTransitionException::class); - $this->expectExceptionMessage('Cannot transition states when an AttributeRedactor is set'); - - $model = Article::factory()->create(); + $auditableType = $morphMap ? 'articles' : Article::class; - $model->attributeModifiers = [ - 'title' => RightRedactor::class, - ]; - - $audit = AuditFactory::new()->create([ - 'auditable_id' => $model->getKey(), - 'auditable_type' => Article::class, + $audits = $models->map(function (Article $model) use ($auditableType, $oldValues, $newValues) { + return AuditFactory::new()->create([ + 'auditable_id' => $model->getKey(), + 'auditable_type' => $auditableType, + 'old_values' => $oldValues, + 'new_values' => $newValues, ]); - - $model->transitionTo($audit); - } - - /** - * @group Auditable::transitionTo - * @test - */ - public function itFailsToTransitionWhenTheAuditableAttributeCompatibilityIsNotMet() - { - $model = Article::factory()->create(); - - $incompatibleAudit = AuditFactory::new()->create([ - 'event' => 'created', - 'auditable_id' => $model->getKey(), - 'auditable_type' => Article::class, - 'old_values' => [], - 'new_values' => [ - 'subject' => 'Culpa qui rerum excepturi quisquam quia officiis.', - 'text' => 'Magnam enim suscipit officiis tempore ut quis harum.', - ], - ]); - - try { - $model->transitionTo($incompatibleAudit); - } catch (AuditableTransitionException $e) { - $this->assertSame( - 'Incompatibility between [Ensi\LaravelAuditing\Tests\Models\Article:1] and [Ensi\LaravelAuditing\Models\Audit:3]', - $e->getMessage() - ); - - Assert::assertArraySubset([ - 'subject', - 'text', - ], $e->getIncompatibilities(), true); - } - } - - /** - * @group Auditable::transitionTo - * @test - * - * @dataProvider auditableTransitionTestProvider - * - * @param bool $morphMap - * @param array $oldValues - * @param array $newValues - * @param array $oldExpectation - * @param array $newExpectation - */ - public function itTransitionsToAnotherModelState( - bool $morphMap, - array $oldValues, - array $newValues, - array $oldExpectation, - array $newExpectation - ) { - $models = Article::factory()->count(2)->create([ - 'title' => 'Facilis voluptas qui impedit deserunt vitae quidem.', - 'content' => 'Consectetur distinctio nihil eveniet cum. Expedita dolores animi dolorum eos repellat rerum.', - ]); - - if ($morphMap) { - Relation::morphMap([ - 'articles' => Article::class, - ]); - } - - $auditableType = $morphMap ? 'articles' : Article::class; - - $audits = $models->map(function (Article $model) use ($auditableType, $oldValues, $newValues) { - return AuditFactory::new()->create([ - 'auditable_id' => $model->getKey(), - 'auditable_type' => $auditableType, - 'old_values' => $oldValues, - 'new_values' => $newValues, - ]); - }); - - // Transition with old values - $this->assertInstanceOf(Auditable::class, $models[0]->transitionTo($audits[0], true)); - $this->assertSame($oldExpectation, $models[0]->getDirty()); - - // Transition with new values - $this->assertInstanceOf(Auditable::class, $models[1]->transitionTo($audits[1])); - $this->assertSame($newExpectation, $models[1]->getDirty()); - } - - /** - * @test - */ - public function itWorksWithStringKeyModels() - { - $model = ApiModel::factory()->create(); - $model->save(); - $model->refresh(); - - $this->assertCount(1, $model->audits); - - $model->content = 'Something else'; - $model->save(); - $model->refresh(); - - $this->assertCount(2, $model->audits); - } - - /** - * @return array - */ - public static function auditableTransitionTestProvider(): array - { - return [ - // - // Audit data and expectations for retrieved event - // - [ - // Morph Map - false, - - // Old values - [], - - // New values - [], - - // Expectation when transitioning with old values - [], - - // Expectation when transitioning with new values - [], - ], - - // - // Audit data and expectations for created/restored event - // - [ - // Morph Map - true, - - // Old values - [], - - // New values - [ - 'title' => 'Nullam egestas interdum eleifend.', - 'content' => 'Morbi consectetur laoreet sem, eu tempus odio tempor id.', - ], - - // Expectation when transitioning with old values - [], - - // Expectation when transitioning with new values - [ - 'title' => 'NULLAM EGESTAS INTERDUM ELEIFEND.', - 'content' => 'Morbi consectetur laoreet sem, eu tempus odio tempor id.', - ], - ], - - // - // Audit data and expectations for updated event - // - [ - // Morph Map - false, - - // Old values - [ - 'title' => 'Vivamus a urna et lorem faucibus malesuada nec nec magna.', - 'content' => 'Mauris ipsum erat, semper non quam vel, sodales tincidunt ligula.', - ], - - // New values - [ - 'title' => 'Nullam egestas interdum eleifend.', - 'content' => 'Morbi consectetur laoreet sem, eu tempus odio tempor id.', - ], - - // Expectation when transitioning with old values - [ - 'title' => 'VIVAMUS A URNA ET LOREM FAUCIBUS MALESUADA NEC NEC MAGNA.', - 'content' => 'Mauris ipsum erat, semper non quam vel, sodales tincidunt ligula.', - ], - - // Expectation when transitioning with new values - [ - 'title' => 'NULLAM EGESTAS INTERDUM ELEIFEND.', - 'content' => 'Morbi consectetur laoreet sem, eu tempus odio tempor id.', - ], - ], - - // - // Audit data and expectations for deleted event - // - [ - // Morph Map - true, - - // Old values - [ - 'title' => 'Vivamus a urna et lorem faucibus malesuada nec nec magna.', - 'content' => 'Mauris ipsum erat, semper non quam vel, sodales tincidunt ligula.', - ], - - // New values - [], - - // Expectation when transitioning with old values - [ - 'title' => 'VIVAMUS A URNA ET LOREM FAUCIBUS MALESUADA NEC NEC MAGNA.', - 'content' => 'Mauris ipsum erat, semper non quam vel, sodales tincidunt ligula.', - ], - - // Expectation when transitioning with new values - [], - ], - ]; - } -} + }); + + // Transition with old values + assertInstanceOf(Auditable::class, $models[0]->transitionTo($audits[0], true)); + assertSame($oldExpectation, $models[0]->getDirty()); + + // Transition with new values + assertInstanceOf(Auditable::class, $models[1]->transitionTo($audits[1])); + assertSame($newExpectation, $models[1]->getDirty()); +})->with([ + // + // Audit data and expectations for retrieved event + // + [ + // Morph Map + false, + + // Old values + [], + + // New values + [], + + // Expectation when transitioning with old values + [], + + // Expectation when transitioning with new values + [], + ], + + // + // Audit data and expectations for created/restored event + // + [ + // Morph Map + true, + + // Old values + [], + + // New values + [ + 'title' => 'Nullam egestas interdum eleifend.', + 'content' => 'Morbi consectetur laoreet sem, eu tempus odio tempor id.', + ], + + // Expectation when transitioning with old values + [], + + // Expectation when transitioning with new values + [ + 'title' => 'NULLAM EGESTAS INTERDUM ELEIFEND.', + 'content' => 'Morbi consectetur laoreet sem, eu tempus odio tempor id.', + ], + ], + + // + // Audit data and expectations for updated event + // + [ + // Morph Map + false, + + // Old values + [ + 'title' => 'Vivamus a urna et lorem faucibus malesuada nec nec magna.', + 'content' => 'Mauris ipsum erat, semper non quam vel, sodales tincidunt ligula.', + ], + + // New values + [ + 'title' => 'Nullam egestas interdum eleifend.', + 'content' => 'Morbi consectetur laoreet sem, eu tempus odio tempor id.', + ], + + // Expectation when transitioning with old values + [ + 'title' => 'VIVAMUS A URNA ET LOREM FAUCIBUS MALESUADA NEC NEC MAGNA.', + 'content' => 'Mauris ipsum erat, semper non quam vel, sodales tincidunt ligula.', + ], + + // Expectation when transitioning with new values + [ + 'title' => 'NULLAM EGESTAS INTERDUM ELEIFEND.', + 'content' => 'Morbi consectetur laoreet sem, eu tempus odio tempor id.', + ], + ], + + // + // Audit data and expectations for deleted event + // + [ + // Morph Map + true, + + // Old values + [ + 'title' => 'Vivamus a urna et lorem faucibus malesuada nec nec magna.', + 'content' => 'Mauris ipsum erat, semper non quam vel, sodales tincidunt ligula.', + ], + + // New values + [], + + // Expectation when transitioning with old values + [ + 'title' => 'VIVAMUS A URNA ET LOREM FAUCIBUS MALESUADA NEC NEC MAGNA.', + 'content' => 'Mauris ipsum erat, semper non quam vel, sodales tincidunt ligula.', + ], + + // Expectation when transitioning with new values + [], + ], +]); + +test('it works with string key models', function () { + $model = ApiModel::factory()->create(); + $model->save(); + $model->refresh(); + + assertCount(1, $model->audits); + + $model->content = 'Something else'; + $model->save(); + $model->refresh(); + + assertCount(2, $model->audits); +}); diff --git a/tests/Unit/TestCases/TransactionRegistryTestCase.php b/tests/Unit/TestCases/TransactionRegistryTestCase.php new file mode 100644 index 0000000..e63037b --- /dev/null +++ b/tests/Unit/TestCases/TransactionRegistryTestCase.php @@ -0,0 +1,32 @@ +mockConnection = $this->mockConnection(); + $this->testing = new TransactionRegistry(self::DEFAULT_CONNECTION_NAME); + } + + protected function mockConnection(): MockInterface|Connection + { + $mockConnection = $this->mock(Connection::class); + $mockConnection->shouldReceive('getName')->andReturn(self::DEFAULT_CONNECTION_NAME); + + return $mockConnection; + } +} diff --git a/tests/Unit/TransactionRegistryTest.php b/tests/Unit/TransactionRegistryTest.php index 5172fec..49ba39b 100644 --- a/tests/Unit/TransactionRegistryTest.php +++ b/tests/Unit/TransactionRegistryTest.php @@ -2,118 +2,85 @@ namespace Ensi\LaravelAuditing\Tests\Unit; -use Ensi\LaravelAuditing\Tests\AuditingTestCase; -use Ensi\LaravelAuditing\Transactions\TransactionRegistry; -use Illuminate\Database\Connection; +use Ensi\LaravelAuditing\Tests\Unit\TestCases\TransactionRegistryTestCase; use Illuminate\Database\Events\TransactionBeginning; use Illuminate\Database\Events\TransactionCommitted; use Illuminate\Database\Events\TransactionRolledBack; -use Mockery; - -class TransactionRegistryTest extends AuditingTestCase -{ - const DEFAULT_CONNECTION_NAME = 'default'; - - private $mockConnection; - private $testing; - - public function setUp(): void - { - parent::setUp(); - - $this->mockConnection = $this->mockConnection(self::DEFAULT_CONNECTION_NAME); - $this->testing = new TransactionRegistry(self::DEFAULT_CONNECTION_NAME); - } - - /** - * @test - */ - public function itRemembersAttributesForConnection(): void - { - $this->assertSame($this->testing->attributes(), $this->testing->attributes()); - $this->assertNotSame($this->testing->attributes(), $this->testing->attributes('other')); - } - - /** - * @test - */ - public function itProcessesBeginTransaction(): void - { - $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); - - $this->assertTrue($this->testing->attributes()->isActive()); - } - - /** - * @test - */ - public function itIgnoresBegunTransaction(): void - { - $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); - $uid = $this->testing->uid(); - - $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); - - $this->assertEquals($uid, $this->testing->uid()); - } - - /** - * @test - */ - public function itProcessesCommitTransaction(): void - { - $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); - $this->mockConnection->shouldReceive('transactionLevel')->andReturn(0); - - $this->testing->onCommit(new TransactionCommitted($this->mockConnection)); - - $this->assertFalse($this->testing->attributes()->isActive()); - } - - /** - * @test - */ - public function itIgnoresCommitSavePoint(): void - { - $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); - $this->mockConnection->shouldReceive('transactionLevel')->andReturn(1); - - $this->testing->onCommit(new TransactionCommitted($this->mockConnection)); - - $this->assertTrue($this->testing->attributes()->isActive()); - } - - /** - * @test - */ - public function itProcessesRollbackTransaction(): void - { - $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); - $this->mockConnection->shouldReceive('transactionLevel')->andReturn(0); - - $this->testing->onRollback(new TransactionRolledBack($this->mockConnection)); - - $this->assertFalse($this->testing->attributes()->isActive()); - } - - /** - * @test - */ - public function itIgnoresRollbackSavePoint(): void - { - $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); - $this->mockConnection->shouldReceive('transactionLevel')->andReturn(1); - - $this->testing->onRollback(new TransactionRolledBack($this->mockConnection)); - - $this->assertTrue($this->testing->attributes()->isActive()); - } - - private function mockConnection(string $name): Mockery\MockInterface|Connection - { - $mockConnection = Mockery::mock(Connection::class); - $mockConnection->shouldReceive('getName')->andReturn($name); - - return $mockConnection; - } -} + +use function PHPUnit\Framework\assertEquals; +use function PHPUnit\Framework\assertFalse; +use function PHPUnit\Framework\assertNotSame; +use function PHPUnit\Framework\assertSame; +use function PHPUnit\Framework\assertTrue; + +uses(TransactionRegistryTestCase::class); + +test('it remembers attributes for connection', function () { + /** @var TransactionRegistryTestCase $this */ + + assertSame($this->testing->attributes(), $this->testing->attributes()); + assertNotSame($this->testing->attributes(), $this->testing->attributes('other')); +}); + +test('it processes begin transaction', function () { + /** @var TransactionRegistryTestCase $this */ + + $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); + + assertTrue($this->testing->attributes()->isActive()); +}); + +test('it ignores begun transaction', function () { + /** @var TransactionRegistryTestCase $this */ + + $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); + $uid = $this->testing->uid(); + + $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); + + assertEquals($uid, $this->testing->uid()); +}); + +test('it processes commit transaction', function () { + /** @var TransactionRegistryTestCase $this */ + + $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); + $this->mockConnection->shouldReceive('transactionLevel')->andReturn(0); + + $this->testing->onCommit(new TransactionCommitted($this->mockConnection)); + + assertFalse($this->testing->attributes()->isActive()); +}); + +test('it ignores commit save point', function () { + /** @var TransactionRegistryTestCase $this */ + + $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); + $this->mockConnection->shouldReceive('transactionLevel')->andReturn(1); + + $this->testing->onCommit(new TransactionCommitted($this->mockConnection)); + + assertTrue($this->testing->attributes()->isActive()); +}); + +test('it processes rollback transaction', function () { + /** @var TransactionRegistryTestCase $this */ + + $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); + $this->mockConnection->shouldReceive('transactionLevel')->andReturn(0); + + $this->testing->onRollback(new TransactionRolledBack($this->mockConnection)); + + assertFalse($this->testing->attributes()->isActive()); +}); + +test('it ignores rollback save point', function () { + /** @var TransactionRegistryTestCase $this */ + + $this->testing->onBegin(new TransactionBeginning($this->mockConnection)); + $this->mockConnection->shouldReceive('transactionLevel')->andReturn(1); + + $this->testing->onRollback(new TransactionRolledBack($this->mockConnection)); + + assertTrue($this->testing->attributes()->isActive()); +});