diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..855d8cb --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,57 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, windows-latest] + php: [8.0, 8.1, 8.2] + laravel: [^8.83.27, ^9.51.0, ^10.0.0] + stability: [prefer-lowest, prefer-stable] + include: + - laravel: ^10.0.0 + testbench: ^8.0.0 + - laravel: ^9.51.0 + testbench: ^7.22.0 + - laravel: ^8.83.27 + testbench: ^6.25.1 + exclude: + - php: 8.0 + laravel: ^10.0.0 + - php: 8.1 + laravel: ^8.83.27 + + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: xdebug + + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + 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 update --${{ matrix.stability }} --prefer-dist --no-interaction + composer require nesbot/carbon:^2.68.1 + + - name: List Installed Dependencies + run: composer show -D + + - name: Execute test + run: composer test -- --ci diff --git a/composer.json b/composer.json index 0743c66..0c8eb1d 100644 --- a/composer.json +++ b/composer.json @@ -22,13 +22,16 @@ } ], "require": { - "php": "^7.3|^8.0", - "laravel/framework": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0" + "php": "^8.0", + "illuminate/contracts": "^8.83.27|^9.51.0|^10.0.0", + "spatie/laravel-package-tools": "^1.12" }, "require-dev": { - "orchestra/testbench": "~3.8|^4.0|^5.0|^7.0|^8.0", + "orchestra/testbench": "^6.25.1|^7.22.0|^8.0.0", "mockery/mockery": "^0.9.4 || ~1.0", - "phpunit/phpunit": "~8.5|^9.0" + "pestphp/pest": "^1.23.1|^2.11", + "pestphp/pest-plugin-laravel": "^1.4|^2.1", + "laravel/pint": "^1.5" }, "autoload": { "psr-4": { @@ -51,6 +54,13 @@ } }, "scripts": { - "test": "vendor/bin/phpunit" + "test": "vendor/bin/pest", + "format": "vendor/bin/pint", + "format-dryrun": "vendor/bin/pint --test" + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } } } diff --git a/src/config/gamify.php b/config/gamify.php similarity index 93% rename from src/config/gamify.php rename to config/gamify.php index b40050a..0fb8f48 100644 --- a/src/config/gamify.php +++ b/config/gamify.php @@ -1,5 +1,7 @@ '\App\User', @@ -33,5 +35,5 @@ ], // Default level - 'badge_default_level' => 1 + 'badge_default_level' => 1, ]; diff --git a/src/migrations/add_reputation_on_user_table.php.stub b/database/migrations/add_reputation_on_user_table.php.stub similarity index 54% rename from src/migrations/add_reputation_on_user_table.php.stub rename to database/migrations/add_reputation_on_user_table.php.stub index 25213db..39cd41c 100644 --- a/src/migrations/add_reputation_on_user_table.php.stub +++ b/database/migrations/add_reputation_on_user_table.php.stub @@ -1,31 +1,23 @@ getTable(), function (Blueprint $table) { $table->unsignedInteger('reputation')->default(0)->after('remember_token'); }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() + public function down(): void { - Schema::table('users', function (Blueprint $table) { + Schema::table(app(config('gamify.payee_model'))->getTable(), function (Blueprint $table) { $table->dropColumn('reputation'); }); } diff --git a/src/migrations/create_gamify_tables.php.stub b/database/migrations/create_gamify_tables.php.stub similarity index 83% rename from src/migrations/create_gamify_tables.php.stub rename to database/migrations/create_gamify_tables.php.stub index d1f05af..2b8da5f 100644 --- a/src/migrations/create_gamify_tables.php.stub +++ b/database/migrations/create_gamify_tables.php.stub @@ -1,19 +1,15 @@ increments('id'); $table->string('name'); @@ -25,7 +21,6 @@ class CreateGamifyTables extends Migration $table->timestamps(); }); - // badges table Schema::create('badges', function (Blueprint $table) { $table->increments('id'); $table->string('name'); @@ -35,7 +30,6 @@ class CreateGamifyTables extends Migration $table->timestamps(); }); - // user_badges pivot Schema::create('user_badges', function (Blueprint $table) { $table->primary(['user_id', 'badge_id']); $table->unsignedInteger('user_id'); @@ -44,12 +38,7 @@ class CreateGamifyTables extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() + public function down(): void { Schema::dropIfExists('user_badges'); Schema::dropIfExists('badges'); diff --git a/phpunit.xml b/phpunit.xml index 9024953..48dda83 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,21 +1,20 @@ - - - - ./tests/ - - - - - - + + + ./tests/Feature + + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..ed80727 --- /dev/null +++ b/pint.json @@ -0,0 +1,92 @@ +{ + "preset": "psr12", + "exclude": [ + "src" + ], + "notPath": [ + ".phpunit.result.cache" + ], + "rules": { + "align_multiline_comment": true, + "array_indentation": true, + "array_syntax": true, + "assign_null_coalescing_to_coalesce_equal": true, + "binary_operator_spaces": true, + "blank_line_before_statement": { + "statements": [ + "break", + "continue", + "declare", + "return", + "throw", + "try" + ] + }, + "cast_spaces": true, + "class_attributes_separation": { + "elements": { + "method": "one" + } + }, + "clean_namespace": true, + "combine_consecutive_issets": true, + "combine_consecutive_unsets": true, + "declare_strict_types": true, + "doctrine_annotation_indentation": true, + "doctrine_annotation_spaces": true, + "fully_qualified_strict_types": true, + "function_typehint_space": true, + "global_namespace_import": true, + "heredoc_indentation": true, + "include": true, + "lambda_not_used_import": true, + "linebreak_after_opening_tag": true, + "list_syntax": true, + "magic_constant_casing": true, + "magic_method_casing": true, + "method_argument_space": { + "on_multiline": "ensure_fully_multiline", + "keep_multiple_spaces_after_comma": true + }, + "method_chaining_indentation": true, + "multiline_comment_opening_closing": true, + "multiline_whitespace_before_semicolons": true, + "native_function_casing": true, + "native_function_type_declaration_casing": true, + "no_alias_language_construct_call": true, + "no_alternative_syntax": true, + "no_empty_comment": true, + "no_empty_statement": true, + "no_extra_blank_lines": true, + "no_leading_namespace_whitespace": true, + "no_mixed_echo_print": true, + "no_multiline_whitespace_around_double_arrow": true, + "no_multiple_statements_per_line": true, + "no_singleline_whitespace_before_semicolons": true, + "no_spaces_around_offset": true, + "no_unneeded_import_alias": true, + "no_unused_imports": true, + "no_whitespace_before_comma_in_array": true, + "no_whitespace_in_blank_line": true, + "not_operator_with_space": true, + "not_operator_with_successor_space": true, + "php_unit_fqcn_annotation": true, + "phpdoc_line_span": { + "const": "single", + "method": "single", + "property": "single" + }, + "phpdoc_scalar": true, + "phpdoc_single_line_var_spacing": true, + "phpdoc_var_without_name": true, + "simple_to_complex_string_variable": true, + "simplified_if_return": true, + "single_quote": true, + "standardize_not_equals": true, + "trailing_comma_in_multiline": true, + "trim_array_spaces": true, + "types_spaces": true, + "unary_operator_spaces": true, + "whitespace_after_comma_in_array": true + } +} diff --git a/src/GamifyServiceProvider.php b/src/GamifyServiceProvider.php index af2335a..7e4e177 100644 --- a/src/GamifyServiceProvider.php +++ b/src/GamifyServiceProvider.php @@ -9,56 +9,36 @@ use QCod\Gamify\Console\MakeBadgeCommand; use QCod\Gamify\Console\MakePointCommand; use QCod\Gamify\Events\ReputationChanged; +use Spatie\LaravelPackageTools\Package; +use Spatie\LaravelPackageTools\PackageServiceProvider; -class GamifyServiceProvider extends ServiceProvider +class GamifyServiceProvider extends PackageServiceProvider { - /** - * Perform post-registration booting of services. - * - * @return void - */ - public function boot() - { - // publish config - $this->publishes([ - __DIR__ . '/config/gamify.php' => config_path('gamify.php'), - ], 'config'); - - $this->mergeConfigFrom(__DIR__ . '/config/gamify.php', 'gamify'); - - // publish migration - if (!class_exists('CreateGamifyTables')) { - $timestamp = date('Y_m_d_His', time()); - $this->publishes([ - __DIR__ . '/migrations/create_gamify_tables.php.stub' => database_path("/migrations/{$timestamp}_create_gamify_tables.php"), - __DIR__ . '/migrations/add_reputation_on_user_table.php.stub' => database_path("/migrations/{$timestamp}_add_reputation_field_on_user_table.php"), - ], 'migrations'); - } - // register commands - if ($this->app->runningInConsole()) { - $this->commands([ + public function configurePackage(Package $package): void + { + $package->name('gamify') + ->hasConfigFile() + ->hasMigrations([ + 'add_reputation_on_user_table', + 'create_gamify_tables', + ]) + ->hasCommands([ MakePointCommand::class, MakeBadgeCommand::class, ]); - } + } - // register event listener + public function packageBooted(): void + { Event::listen(ReputationChanged::class, SyncBadges::class); } - /** - * Register bindings in the container. - * - * @return void - */ - public function register() + public function packageRegistered(): void { $this->app->singleton('badges', function () { return cache()->rememberForever('gamify.badges.all', function () { - return $this->getBadges()->map(function ($badge) { - return new $badge; - }); + return $this->getBadges()->map(fn($badge) => new $badge); }); }); } @@ -66,9 +46,9 @@ public function register() /** * Get all the badge inside app/Gamify/Badges folder * - * @return Collection + * @return Collection */ - protected function getBadges() + protected function getBadges(): Collection { $badgeRootNamespace = config( 'gamify.badge_namespace', diff --git a/src/HasBadges.php b/src/HasBadges.php index 6cdd50f..0f9e7f6 100644 --- a/src/HasBadges.php +++ b/src/HasBadges.php @@ -2,6 +2,10 @@ namespace QCod\Gamify; +/** + * @property-read \Illuminate\Database\Eloquent\Collection $badges + * @property-read int|null $badges_count + */ trait HasBadges { /** diff --git a/src/HasReputations.php b/src/HasReputations.php index 8b46717..b0410cf 100644 --- a/src/HasReputations.php +++ b/src/HasReputations.php @@ -4,6 +4,10 @@ use QCod\Gamify\Events\ReputationChanged; +/** + * @property-read \Illuminate\Database\Eloquent\Collection $reputations + * @property-read int|null $reputations_count + */ trait HasReputations { /** diff --git a/tests/BadgeTest.php b/tests/BadgeTest.php deleted file mode 100644 index 5df4071..0000000 --- a/tests/BadgeTest.php +++ /dev/null @@ -1,77 +0,0 @@ -createUser(); - $badge = $this->createBadge(); - - $badge->awardTo($user); - - $this->assertCount(1, $user->badges); - $this->assertTrue($user->badges->first()->is($badge)); - } - - /** - * a badge can be remove from a user - * - * @test - */ - public function a_badge_can_be_remove_from_a_user() - { - $user = $this->createUser(); - $badge = $this->createBadge(); - $badge->awardTo($user); - $this->assertCount(1, $user->badges); - - $badge->removeFrom($user); - - $this->assertCount(0, $user->fresh()->badges); - } - - /** - * a badge is awarded if user point reached 1000 - * - * @test - */ - public function a_badge_is_awarded_if_user_point_reached_1000() - { - $user = $this->createUser(); - $this->assertCount(0, $user->badges); - - $user->addPoint(1001); - - $this->assertCount(1, $user->fresh()->badges); - - $user->reducePoint(10); - - $this->assertCount(0, $user->fresh()->badges); - } - - /** - * a badge is given when user first creats a post - * - * @test - */ - public function a_badge_is_given_when_user_first_creats_a_post() - { - $user = $this->createUser(); - $this->assertCount(0, $user->badges); - - $this->createPost(['user_id' => $user->id]); - $this->assertCount(0, $user->fresh()->badges); - - $user->addPoint(20); - $this->assertCount(1, $user->fresh()->badges); - - $this->assertEquals('First Contribution', $user->fresh()->badges->first()->name); - } -} diff --git a/tests/Feature/BadgeTest.php b/tests/Feature/BadgeTest.php new file mode 100644 index 0000000..626e586 --- /dev/null +++ b/tests/Feature/BadgeTest.php @@ -0,0 +1,57 @@ +awardTo($user); + + assertCount(1, $user->badges); + assertTrue($user->badges->first()->is($badge)); +}); + +test('a badge can be remove from a user', function () { + $user = createUser(); + $badge = createBadge(); + $badge->awardTo($user); + assertCount(1, $user->badges); + + $badge->removeFrom($user); + + assertCount(0, $user->fresh()->badges); +}); + +test( + 'a badge is awarded if user point reached 1000', + function () { + $user = createUser(); + assertCount(0, $user->badges); + + $user->addPoint(1001); + + assertCount(1, $user->fresh()->badges); + + $user->reducePoint(10); + + assertCount(0, $user->fresh()->badges); + } +); + +test('a badge is given when user first creat a post', function () { + $user = createUser(); + assertCount(0, $user->badges); + + createPost(['user_id' => $user->id]); + assertCount(0, $user->fresh()->badges); + + $user->addPoint(20); + assertCount(1, $user->fresh()->badges); + + assertEquals('First Contribution', $user->fresh()->badges->first()->name); +}); diff --git a/tests/Feature/PointTest.php b/tests/Feature/PointTest.php new file mode 100644 index 0000000..4bb2aec --- /dev/null +++ b/tests/Feature/PointTest.php @@ -0,0 +1,174 @@ +getName()); +}); + +it('uses name property for point name if provided', function () { + $point = new FakeWelcomeUserWithNamePoint(1); + + assertEquals('FakeName', $point->getName()); +}); + +it('can get points for a point type', function () { + $point = new FakeCreatePostPoint(1); + + assertEquals(10, $point->getPoints()); +}); + +it('gives point to a user', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + + $user->givePoint(new FakeCreatePostPoint($post)); + + assertEquals(10, $user->fresh()->getPoints()); + assertCount(1, $user->reputations); + assertDatabaseHas('reputations', [ + 'payee_id' => $user->id, + 'subject_type' => $post->getMorphClass(), + 'subject_id' => $post->id, + 'point' => 10, + 'name' => 'FakeCreatePostPoint', + ]); +}); + +it('can access a reputation payee and subject', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + + $user->givePoint(new FakeCreatePostPoint($post)); + + $point = $user->reputations()->first(); + + assertEquals($user->id, $point->payee->id); + assertEquals($post->id, $point->subject->id); + + assertEquals('FakeCreatePostPoint', $post->reputations->first()->name); +}); + +it('only adds unique point reward if property is set on point type', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + + $user->givePoint(new FakeCreatePostPoint($post)); + $user->givePoint(new FakeCreatePostPoint($post)); + + assertEquals(10, $user->fresh()->getPoints()); + assertCount(1, $user->reputations); +}); + +it('can store duplicate reputations if no property set', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + + $user->givePoint(new FakeWelcomeUserWithNamePoint($post)); + $user->givePoint(new FakeWelcomeUserWithNamePoint($post)); + + assertEquals(60, $user->fresh()->getPoints()); + assertCount(2, $user->reputations); +}); + +it('do not give point if qualifier returns false', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + + $user->givePoint(new FakeWelcomeUserWithFalseQualifier($post)); + + assertEquals(0, $user->fresh()->getPoints()); + assertCount(0, $user->reputations); +}); + +it('uses payee field on point as relation if no payee method override', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + + $user->givePoint(new FakePayeeFieldPoint($post)); + + assertEquals(10, $user->fresh()->getPoints()); + assertCount(1, $user->reputations); +}); + +it('can undo a reward by given model', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + $user->givePoint(new FakeWelcomeUserWithNamePoint($post)); + $user->givePoint(new FakeWelcomeUserWithNamePoint($post)); + assertEquals(60, $user->fresh()->getPoints()); + assertCount(2, $user->reputations); + + $user->undoPoint(new FakeWelcomeUserWithNamePoint($post)); + + assertEquals(30, $user->fresh()->getPoints()); + assertCount(1, $user->fresh()->reputations); + + $user->undoPoint(new FakeWelcomeUserWithNamePoint($post)); + + assertEquals(0, $user->fresh()->getPoints()); + assertCount(0, $user->fresh()->reputations); +}); + +it('throws exception if no payee is returned', function () { + $user = createUser(); + $user->givePoint(new FakePointTypeWithoutPayee()); + + assertEquals(0, $user->fresh()->getPoints()); + assertCount(0, $user->reputations); +}) + ->throws(InvalidPayeeModel::class); + +it('throws exception if no subject is set', function () { + $user = createUser(); + + $user->givePoint(new FakePointTypeWithoutSubject()); + + assertEquals(0, $user->fresh()->getPoints()); + assertCount(0, $user->reputations); +}) + ->throws(PointSubjectNotSet::class); + +it('throws exception if no points field or method is defined', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + + $user->givePoint(new FakePointWithoutPoint($post)); + + assertEquals(0, $user->fresh()->getPoints()); + assertCount(0, $user->reputations); +}) + ->throws(PointsNotDefined::class); + +it('gives and undo point via helper functions', function () { + $user = createUser(); + $post = createPost(['user_id' => $user->id]); + + givePoint(new FakePayeeFieldPoint($post), $user); + + assertEquals(10, $user->fresh()->getPoints()); + assertCount(1, $user->reputations); + + undoPoint(new FakePayeeFieldPoint($post), $user); + + $user = $user->fresh(); + assertEquals(0, $user->getPoints()); + assertCount(0, $user->reputations); +}); diff --git a/tests/Feature/ReputationTest.php b/tests/Feature/ReputationTest.php new file mode 100644 index 0000000..4a9109d --- /dev/null +++ b/tests/Feature/ReputationTest.php @@ -0,0 +1,73 @@ + 10]); + + assertEquals(10, $user->getPoints()); +}); + +it('gives reputation point to a user', function () { + $user = createUser(); + assertEquals(0, $user->getPoints()); + + $user->addPoint(10); + + assertEquals(10, $user->fresh()->getPoints()); +}); + +it('reduces reputation point for a user', function () { + $user = createUser(['reputation' => 20]); + assertEquals(20, $user->reputation); + + $user->reducePoint(5); + + assertEquals(15, $user->fresh()->getPoints()); +}); + +it('zeros reputation point of a user', function () { + $user = createUser(['reputation' => 50]); + assertEquals(50, $user->getPoints()); + + $user->resetPoint(); + + assertEquals(0, $user->fresh()->getPoints()); +}); + +it('fires event on reputation change', function () { + Event::fake(); + + $user = createUser(); + assertEquals(0, $user->getPoints()); + + $user->addPoint(10); + + Event::assertDispatched(ReputationChanged::class, function ($event) use ($user) { + return ($event->point === 10 && $user->id == $event->user->id && $event->increment); + }); + + assertEquals(10, $user->fresh()->getPoints()); +}); + +it('fires event on reputation reduced', function () { + Event::fake(); + + $user = createUser(['reputation' => 10]); + + $user->reducePoint(3); + + Event::assertDispatched(ReputationChanged::class, function ($event) use ($user) { + return ($event->point === 3 && $user->id == $event->user->id && ! $event->increment); + }); + + assertEquals(7, $user->fresh()->getPoints()); +}); diff --git a/tests/Badges/FirstContribution.php b/tests/Fixtures/Badges/FirstContribution.php similarity index 79% rename from tests/Badges/FirstContribution.php rename to tests/Fixtures/Badges/FirstContribution.php index bbbba16..367324e 100644 --- a/tests/Badges/FirstContribution.php +++ b/tests/Fixtures/Badges/FirstContribution.php @@ -1,9 +1,11 @@ subject = $subject; + } + + public function payee() + { + return $this->getSubject()->user; + } +} diff --git a/tests/Fixtures/Fake/PointTypes/FakePayeeFieldPoint.php b/tests/Fixtures/Fake/PointTypes/FakePayeeFieldPoint.php new file mode 100644 index 0000000..a12e1a9 --- /dev/null +++ b/tests/Fixtures/Fake/PointTypes/FakePayeeFieldPoint.php @@ -0,0 +1,20 @@ +subject = $subject; + } + + /** @var string payee model relation on subject */ + protected $payee = 'user'; +} diff --git a/tests/Fixtures/Fake/PointTypes/FakePointTypeWithoutPayee.php b/tests/Fixtures/Fake/PointTypes/FakePointTypeWithoutPayee.php new file mode 100644 index 0000000..7b9c5b2 --- /dev/null +++ b/tests/Fixtures/Fake/PointTypes/FakePointTypeWithoutPayee.php @@ -0,0 +1,16 @@ +subject = $subject; + } +} diff --git a/tests/Fixtures/Fake/PointTypes/FakeWelcomeUserWithFalseQualifier.php b/tests/Fixtures/Fake/PointTypes/FakeWelcomeUserWithFalseQualifier.php new file mode 100644 index 0000000..3002bd6 --- /dev/null +++ b/tests/Fixtures/Fake/PointTypes/FakeWelcomeUserWithFalseQualifier.php @@ -0,0 +1,27 @@ +subject = $subject; + } + + public function qualifier() + { + return false; + } + + public function payee() + { + return $this->getSubject()->user; + } +} diff --git a/tests/Fixtures/Fake/PointTypes/FakeWelcomeUserWithNamePoint.php b/tests/Fixtures/Fake/PointTypes/FakeWelcomeUserWithNamePoint.php new file mode 100644 index 0000000..a0295fe --- /dev/null +++ b/tests/Fixtures/Fake/PointTypes/FakeWelcomeUserWithNamePoint.php @@ -0,0 +1,24 @@ +subject = $subject; + } + + public function payee() + { + return $this->getSubject()->user; + } +} diff --git a/tests/Fixtures/Models/Post.php b/tests/Fixtures/Models/Post.php new file mode 100644 index 0000000..95ed0d8 --- /dev/null +++ b/tests/Fixtures/Models/Post.php @@ -0,0 +1,46 @@ + $reputations + */ +class Post extends Model +{ + public $table = 'test_posts'; + + protected $guarded = []; + + public function replies(): HasMany + { + return $this->hasMany(Reply::class)->latest(); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function bestReply(): HasOne + { + return $this->hasOne(Reply::class, 'id', 'best_reply_id'); + } + + public function reputations(): MorphMany + { + return $this->morphMany(Reputation::class, 'subject'); + } +} diff --git a/tests/Fixtures/Models/Reply.php b/tests/Fixtures/Models/Reply.php new file mode 100644 index 0000000..0a824af --- /dev/null +++ b/tests/Fixtures/Models/Reply.php @@ -0,0 +1,30 @@ +belongsTo(User::class); + } + + public function post(): BelongsTo + { + return $this->belongsTo(Post::class); + } +} diff --git a/tests/Fixtures/Models/User.php b/tests/Fixtures/Models/User.php new file mode 100644 index 0000000..1b03076 --- /dev/null +++ b/tests/Fixtures/Models/User.php @@ -0,0 +1,27 @@ + $posts + */ +class User extends Model +{ + use Gamify; + + public $table = 'test_users'; + + protected $guarded = []; + + public function posts(): HasMany + { + return $this->hasMany(Post::class); + } +} diff --git a/tests/Helpers.php b/tests/Helpers.php new file mode 100644 index 0000000..3363032 --- /dev/null +++ b/tests/Helpers.php @@ -0,0 +1,46 @@ +forceFill(array_merge($attributes, [ + 'name' => 'Saqueib', + 'email' => 'me@example.com', + 'password' => 'secret', + ]))->save(); + + return $user->fresh(); +} + +function createPost(array $attributes = []): Post +{ + $post = new Post(); + + $post->forceFill(array_merge($attributes, [ + 'title' => 'Dummy post title', + 'body' => 'I am the content on dummy post', + 'user_id' => 1, + ]))->save(); + + return $post->fresh(); +} + +function createBadge(array $attributes = []): Badge +{ + $badge = new Badge(); + + $badge->forceFill(array_merge($attributes, [ + 'name' => 'New Member', + 'description' => 'Welcome new user', + 'icon' => 'images/new-member-icon.svg', + ]))->save(); + + return $badge->fresh(); +} diff --git a/tests/Models/Post.php b/tests/Models/Post.php deleted file mode 100644 index ffbf963..0000000 --- a/tests/Models/Post.php +++ /dev/null @@ -1,33 +0,0 @@ -hasMany(Reply::class)->latest(); - } - - public function user() - { - return $this->belongsTo(User::class); - } - - public function bestReply() - { - return $this->hasOne(Reply::class, 'id', 'best_reply_id'); - } - - public function reputations() - { - return $this->morphMany('QCod\Gamify\Reputation', 'subject'); - } -} diff --git a/tests/Models/Reply.php b/tests/Models/Reply.php deleted file mode 100644 index ae0d711..0000000 --- a/tests/Models/Reply.php +++ /dev/null @@ -1,21 +0,0 @@ -belongsTo(User::class); - } - - public function post() - { - return $this->belongsTo(Post::class); - } -} diff --git a/tests/Models/User.php b/tests/Models/User.php deleted file mode 100644 index b10bce5..0000000 --- a/tests/Models/User.php +++ /dev/null @@ -1,20 +0,0 @@ -hasMany(Post::class); - } -} diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..c582fef --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,10 @@ +in('Feature'); diff --git a/tests/PointTest.php b/tests/PointTest.php deleted file mode 100644 index 13443e3..0000000 --- a/tests/PointTest.php +++ /dev/null @@ -1,351 +0,0 @@ -assertEquals('FakeCreatePostPoint', $point->getName()); - } - - /** - * it uses name property for point name if provided - * - * @test - */ - public function it_uses_name_property_for_point_name_if_provided() - { - $point = new FakeWelcomeUserWithNamePoint(1); - - $this->assertEquals('FakeName', $point->getName()); - } - - /** - * it can get points for a point type - * - * @test - */ - public function it_can_get_points_for_a_point_type() - { - $point = new FakeCreatePostPoint(1); - - $this->assertEquals(10, $point->getPoints()); - } - - /** - * it gives point to a user - * - * @test - */ - public function it_gives_point_to_a_user() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - - $user->givePoint(new FakeCreatePostPoint($post)); - - $this->assertEquals(10, $user->fresh()->getPoints()); - $this->assertCount(1, $user->reputations); - $this->assertDatabaseHas('reputations', [ - 'payee_id' => $user->id, - 'subject_type' => $post->getMorphClass(), - 'subject_id' => $post->id, - 'point' => 10, - 'name' => 'FakeCreatePostPoint' - ]); - } - - /** - * it can access a reputation payee and subject - * - * @test - */ - public function it_can_access_a_reputation_payee_and_subject() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - - $user->givePoint(new FakeCreatePostPoint($post)); - - $point = $user->reputations()->first(); - - $this->assertEquals($user->id, $point->payee->id); - $this->assertEquals($post->id, $point->subject->id); - - $this->assertEquals('FakeCreatePostPoint', $post->reputations->first()->name); - } - - /** - * it only adds unique point reward if property is set on point type - * - * @test - */ - public function it_only_adds_unique_point_reward_if_property_is_set_on_point_type() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - - $user->givePoint(new FakeCreatePostPoint($post)); - $user->givePoint(new FakeCreatePostPoint($post)); - - $this->assertEquals(10, $user->fresh()->getPoints()); - $this->assertCount(1, $user->reputations); - } - - /** - * it can store duplicate reputations if no property set - * - * @test - */ - public function it_can_store_duplicate_reputations_if_no_property_set() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - - $user->givePoint(new FakeWelcomeUserWithNamePoint($post)); - $user->givePoint(new FakeWelcomeUserWithNamePoint($post)); - - $this->assertEquals(60, $user->fresh()->getPoints()); - $this->assertCount(2, $user->reputations); - } - - /** - * it do not give point if qualifier returns false - * - * @test - */ - public function it_do_not_give_point_if_qualifier_returns_false() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - - $user->givePoint(new FakeWelcomeUserWithFalseQualifier($post)); - - $this->assertEquals(0, $user->fresh()->getPoints()); - $this->assertCount(0, $user->reputations); - } - - /** - * it uses payee field on point as relation if no payee method override - * - * @test - */ - public function it_uses_payee_field_on_point_as_relation_if_no_payee_method_override() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - - $user->givePoint(new FakePayeeFieldPoint($post)); - - $this->assertEquals(10, $user->fresh()->getPoints()); - $this->assertCount(1, $user->reputations); - } - - /** - * it can undo a reward by given model - * - * @test - */ - public function it_can_undo_a_reward_by_given_model() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - $user->givePoint(new FakeWelcomeUserWithNamePoint($post)); - $user->givePoint(new FakeWelcomeUserWithNamePoint($post)); - $this->assertEquals(60, $user->fresh()->getPoints()); - $this->assertCount(2, $user->reputations); - - $user->undoPoint(new FakeWelcomeUserWithNamePoint($post)); - - $this->assertEquals(30, $user->fresh()->getPoints()); - $this->assertCount(1, $user->fresh()->reputations); - - $user->undoPoint(new FakeWelcomeUserWithNamePoint($post)); - - $this->assertEquals(0, $user->fresh()->getPoints()); - $this->assertCount(0, $user->fresh()->reputations); - } - - /** - * it throws exception if no payee is returned - * - * @test - */ - public function it_throws_exception_if_no_payee_is_returned() - { - $user = $this->createUser(); - $this->expectException(InvalidPayeeModel::class); - - $user->givePoint(new FakePointTypeWithoutPayee()); - - $this->assertEquals(0, $user->fresh()->getPoints()); - $this->assertCount(0, $user->reputations); - } - - /** - * it throws exception if no subject is set - * - * @test - */ - public function it_throws_exception_if_no_subject_is_set() - { - $user = $this->createUser(); - $this->expectException(PointSubjectNotSet::class); - - $user->givePoint(new FakePointTypeWithoutSubject()); - - $this->assertEquals(0, $user->fresh()->getPoints()); - $this->assertCount(0, $user->reputations); - } - - /** - * it throws exception if no points field or method is defined - * - * @test - */ - public function it_throws_exception_if_no_points_field_or_method_is_defined() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - $this->expectException(PointsNotDefined::class); - - $user->givePoint(new FakePointWithoutPoint($post)); - - $this->assertEquals(0, $user->fresh()->getPoints()); - $this->assertCount(0, $user->reputations); - } - - /** - * it gives and undo point via helper functions - * - * @test - */ - public function it_gives_and_undo_point_via_helper_functions() - { - $user = $this->createUser(); - $post = $this->createPost(['user_id' => $user->id]); - - givePoint(new FakePayeeFieldPoint($post), $user); - - $this->assertEquals(10, $user->fresh()->getPoints()); - $this->assertCount(1, $user->reputations); - - undoPoint(new FakePayeeFieldPoint($post), $user); - - $user = $user->fresh(); - $this->assertEquals(0, $user->getPoints()); - $this->assertCount(0, $user->reputations); - } -} - -class FakeCreatePostPoint extends PointType -{ - protected $points = 10; - - public $allowDuplicates = false; - - public function __construct($subject) - { - $this->subject = $subject; - } - - public function payee() - { - return $this->getSubject()->user; - } -} - -class FakeWelcomeUserWithNamePoint extends PointType -{ - protected $name = 'FakeName'; - - protected $points = 30; - - public function __construct($subject) - { - $this->subject = $subject; - } - - public function payee() - { - return $this->getSubject()->user; - } -} - -class FakeWelcomeUserWithFalseQualifier extends PointType -{ - protected $points = 10; - - public function __construct($subject) - { - $this->subject = $subject; - } - - public function qualifier() - { - return false; - } - - public function payee() - { - return $this->getSubject()->user; - } -} - -class FakePointTypeWithoutPayee extends PointType -{ - protected $point = 24; - - public function payee() - { - } -} - -class FakePointTypeWithoutSubject extends PointType -{ - protected $point = 12; - - public function payee() - { - return new User(); - } -} - -class FakePointWithoutPoint extends PointType -{ - protected $payee = 'user'; - - public function __construct($subject) - { - $this->subject = $subject; - } -} - -class FakePayeeFieldPoint extends PointType -{ - protected $points = 10; - - public function __construct($subject) - { - $this->subject = $subject; - } - - /** - * @var string payee model relation on subject - */ - protected $payee = 'user'; -} diff --git a/tests/ReputationTest.php b/tests/ReputationTest.php deleted file mode 100644 index f51a83b..0000000 --- a/tests/ReputationTest.php +++ /dev/null @@ -1,172 +0,0 @@ -createUser(['reputation' => 10]); - - $this->assertEquals(10, $user->getPoints()); - } - - /** - * it gives reputation point to a user - * - * @test - */ - public function it_gives_reputation_point_to_a_user() - { - $user = $this->createUser(); - $this->assertEquals(0, $user->getPoints()); - - $user->addPoint(10); - - $this->assertEquals(10, $user->fresh()->getPoints()); - } - - /** - * it reduces reputation point for a user - * - * @test - */ - public function it_reduces_reputation_point_for_a_user() - { - $user = $this->createUser(['reputation' => 20]); - $this->assertEquals(20, $user->reputation); - - $user->reducePoint(5); - - $this->assertEquals(15, $user->fresh()->getPoints()); - } - - /** - * it zeros reputation point of a user - * - * @test - */ - public function it_zeros_reputation_point_of_a_user() - { - $user = $this->createUser(['reputation' => 50]); - $this->assertEquals(50, $user->getPoints()); - - $user->resetPoint(); - - $this->assertEquals(0, $user->fresh()->getPoints()); - } - - /** - * it fires event on reputation change - * - * @test - */ - public function it_fires_event_on_reputation_change() - { - Event::fake(); - - $user = $this->createUser(); - $this->assertEquals(0, $user->getPoints()); - - $user->addPoint(10); - - Event::assertDispatched(ReputationChanged::class, function ($event) use ($user) { - return ($event->point === 10 && $user->id == $event->user->id && $event->increment); - }); - - $this->assertEquals(10, $user->fresh()->getPoints()); - } - - /** - * it fires event on reputation reduced - * - * @test - */ - public function it_fires_event_on_reputation_reduced() - { - Event::fake(); - - $user = $this->createUser(['reputation' => 10]); - - $user->reducePoint(3); - - Event::assertDispatched(ReputationChanged::class, function ($event) use ($user) { - return ($event->point === 3 && $user->id == $event->user->id && !$event->increment); - }); - - $this->assertEquals(7, $user->fresh()->getPoints()); - } -} - -class FakePostCreated extends PointType -{ - protected $points = 10; - - public function __construct($model) - { - $this->setSubject($model); - } - - /** - * Check qualification for this point - * - * @return bool - */ - public function qualifier() - { - return true; - } - - /** - * User who will be recieving point - * - * @return Model - */ - public function payee() - { - return $this->getSubject()->user; - } -} - -class FakeBestReply extends PointType -{ - protected $points = 50; - - public function __construct($model) - { - $this->setSubject($model); - } - - /** - * Check qualification for this point - * - * @return bool - */ - public function qualifier() - { - return !is_null($this->getSubject()->best_reply_id); - } - - /** - * User who will be recieving point - * - * @return Model - */ - public function payee() - { - return $this->getSubject()->bestReply->user; - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 05db068..ea41459 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,28 +1,66 @@ setUpDatabase($this->app); + } + + protected function setUpDatabase($app): void + { + $schema = $app['db']->connection()->getSchemaBuilder(); + + $schema->create((new Post())->getTable(), function (Blueprint $table) { + $table->increments('id'); + $table->string('title'); + $table->text('body'); + $table->unsignedInteger('best_reply_id')->nullable(); + $table->unsignedInteger('user_id'); + $table->timestamps(); + }); + $schema->create((new Reply())->getTable(), function (Blueprint $table) { + $table->increments('id'); + $table->text('body'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('post_id'); + $table->timestamps(); + }); + $schema->create((new User())->getTable(), function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + include_once __DIR__.'/../database/migrations/add_reputation_on_user_table.php.stub'; + (new AddReputationFieldOnUserTable())->up(); - $this->loadMigrationsFrom(__DIR__ . '/database/migrations'); + include_once __DIR__.'/../database/migrations/create_gamify_tables.php.stub'; + (new CreateGamifyTables())->up(); } - /** - * @param \Illuminate\Foundation\Application $app - */ - protected function getEnvironmentSetUp($app) + /** @param \Illuminate\Foundation\Application $app */ + protected function getEnvironmentSetUp($app): void { $app['config']->set('database.default', 'testbench'); $app['config']->set('database.connections.testbench', [ @@ -31,80 +69,16 @@ protected function getEnvironmentSetUp($app) 'prefix' => '', ]); - $app['config']->set('gamify.payee_model', '\QCod\Gamify\Tests\Models\User'); + $app['config']->set('gamify.payee_model', User::class); - // test badges $app->singleton('badges', function () { - return collect(['FirstContribution', 'FirstThousandPoints']) - ->map(function ($badge) { - return app("QCod\\Gamify\\Tests\Badges\\".$badge); - }); + return collect([FirstContribution::class, FirstThousandPoints::class]) + ->map(fn (string $badge) => app($badge)); }); } - /** - * @param \Illuminate\Foundation\Application $app - * @return array - */ - protected function getPackageProviders($app) - { - return ['QCod\Gamify\GamifyServiceProvider']; - } - - /** - * Create a user - * - * @param array $attrs - * @return User - */ - public function createUser($attrs = []) + protected function getPackageProviders($app): array { - $user = new User(); - - $user->forceFill(array_merge($attrs, [ - 'name' => 'Saqueib', - 'email' => 'me@example.com', - 'password' => 'secret' - ]))->save(); - - return $user->fresh(); - } - - /** - * Create a post - * - * @param array $attrs - * @return Post - */ - public function createPost($attrs = []) - { - $post = new Post(); - - $post->forceFill(array_merge($attrs, [ - 'title' => 'Dummy post title', - 'body' => 'I am the content on dummy post', - 'user_id' => 1 - ]))->save(); - - return $post->fresh(); - } - - /** - * Create a badge - * - * @param array $attrs - * @return Badge - */ - public function createBadge($attrs = []) - { - $badge = new Badge(); - - $badge->forceFill(array_merge($attrs, [ - 'name' => 'New Member', - 'description' => 'Welcome new user', - 'icon' => 'images/new-member-icon.svg', - ]))->save(); - - return $badge->fresh(); + return [GamifyServiceProvider::class]; } } diff --git a/tests/database/migrations/2018_09_10_104820_create_test_posts_table.php b/tests/database/migrations/2018_09_10_104820_create_test_posts_table.php deleted file mode 100644 index 66e3c9f..0000000 --- a/tests/database/migrations/2018_09_10_104820_create_test_posts_table.php +++ /dev/null @@ -1,44 +0,0 @@ -increments('id'); - $table->string('title'); - $table->text('body'); - $table->unsignedInteger('best_reply_id')->nullable(); - $table->unsignedInteger('user_id'); - $table->timestamps(); - }); - - Schema::create('replies', function (Blueprint $table) { - $table->increments('id'); - $table->text('body'); - $table->unsignedInteger('user_id'); - $table->unsignedInteger('post_id'); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('posts'); - Schema::dropIfExists('replies'); - } -} diff --git a/tests/database/migrations/2018_09_17_104820_create_test_users_table.php b/tests/database/migrations/2018_09_17_104820_create_test_users_table.php deleted file mode 100644 index 0640c7b..0000000 --- a/tests/database/migrations/2018_09_17_104820_create_test_users_table.php +++ /dev/null @@ -1,37 +0,0 @@ -increments('id'); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->unsignedInteger('reputation')->default(0); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('users'); - } -} diff --git a/tests/database/migrations/2018_11_06_045032_create_test_reputations_table.php b/tests/database/migrations/2018_11_06_045032_create_test_reputations_table.php deleted file mode 100644 index bce464b..0000000 --- a/tests/database/migrations/2018_11_06_045032_create_test_reputations_table.php +++ /dev/null @@ -1,37 +0,0 @@ -increments('id'); - $table->string('name'); - $table->mediumInteger('point', false)->default(0); - $table->integer('subject_id')->nullable(); - $table->string('subject_type')->nullable(); - $table->unsignedInteger('payee_id')->nullable(); - $table->text('meta')->nullable(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('reputations'); - } -} diff --git a/tests/database/migrations/2018_11_06_205032_create_test_badges_table.php b/tests/database/migrations/2018_11_06_205032_create_test_badges_table.php deleted file mode 100644 index 48b47c5..0000000 --- a/tests/database/migrations/2018_11_06_205032_create_test_badges_table.php +++ /dev/null @@ -1,43 +0,0 @@ -increments('id'); - $table->string('name'); - $table->string('description')->nullable(); - $table->string('icon')->nullable(); - $table->tinyInteger('level')->default(config('gamify.badge_default_level', 1)); - $table->timestamps(); - }); - - Schema::create('user_badges', function (Blueprint $table) { - $table->primary(['user_id', 'badge_id']); - $table->unsignedInteger('user_id'); - $table->unsignedInteger('badge_id'); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('user_badges'); - Schema::dropIfExists('badges'); - } -}