diff --git a/src/composer.json b/src/composer.json index b1c874bf..6d67401d 100644 --- a/src/composer.json +++ b/src/composer.json @@ -1,88 +1,84 @@ { "name": "laravel/laravel", "type": "project", "description": "The Laravel Framework.", "keywords": [ "framework", "laravel" ], "license": "MIT", "repositories": [ { "type": "vcs", "url": "https://git.kolab.org/diffusion/PNL/php-net_ldap3.git" } ], "require": { "php": "^7.3", "barryvdh/laravel-dompdf": "^0.8.6", "doctrine/dbal": "^2.13", "dyrynda/laravel-nullable-fields": "*", "fideloper/proxy": "^4.0", "guzzlehttp/guzzle": "^7.3", "kolab/net_ldap3": "dev-master", "laravel/framework": "6.*", "laravel/horizon": "^3", "laravel/tinker": "^2.4", "mollie/laravel-mollie": "^2.9", "morrislaptop/laravel-queue-clear": "^1.2", "silviolleite/laravelpwa": "^2.0", "spatie/laravel-translatable": "^4.2", "spomky-labs/otphp": "~4.0.0", "stripe/stripe-php": "^7.29", "swooletw/laravel-swoole": "^2.6", "tymon/jwt-auth": "^1.0" }, "require-dev": { - "beyondcode/laravel-dump-server": "^1.0", "beyondcode/laravel-er-diagram-generator": "^1.3", "code-lts/doctum": "^5.1", - "filp/whoops": "^2.0", - "fzaninotto/faker": "^1.4", "kirschbaum-development/mail-intercept": "^0.2.4", "laravel/dusk": "~6.15.0", - "mockery/mockery": "^1.0", "nunomaduro/larastan": "^0.7", "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^8" + "phpunit/phpunit": "^9" }, "config": { "optimize-autoloader": true, "preferred-install": "dist", "sort-packages": true }, "extra": { "laravel": { "dont-discover": [] } }, "autoload": { "psr-4": { "App\\": "app/" }, "classmap": [ "database/seeds", "database/factories", "include" ] }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, "minimum-stability": "dev", "prefer-stable": true, "scripts": { "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" ], "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ "@php artisan key:generate --ansi" ] } } diff --git a/src/database/factories/UserFactory.php b/src/database/factories/UserFactory.php deleted file mode 100644 index b121f4b2..00000000 --- a/src/database/factories/UserFactory.php +++ /dev/null @@ -1,27 +0,0 @@ -define( - User::class, - function (Faker $faker) { - return [ - 'email' => $faker->unique()->safeEmail, - 'password' => Str::random(64) - ]; - } -); diff --git a/src/database/seeds/local/UserSeeder.php b/src/database/seeds/local/UserSeeder.php index 44583ba2..8aace6ff 100644 --- a/src/database/seeds/local/UserSeeder.php +++ b/src/database/seeds/local/UserSeeder.php @@ -1,204 +1,202 @@ 'kolab.org', 'status' => Domain::STATUS_NEW + Domain::STATUS_ACTIVE + Domain::STATUS_CONFIRMED + Domain::STATUS_VERIFIED, 'type' => Domain::TYPE_EXTERNAL ] ); $john = User::create( [ 'email' => 'john@kolab.org', 'password' => \App\Utils::generatePassphrase() ] ); $john->setSettings( [ 'first_name' => 'John', 'last_name' => 'Doe', 'currency' => 'USD', 'country' => 'US', 'billing_address' => "601 13th Street NW\nSuite 900 South\nWashington, DC 20005", 'external_email' => 'john.doe.external@gmail.com', 'organization' => 'Kolab Developers', 'phone' => '+1 509-248-1111', ] ); $john->setAliases(['john.doe@kolab.org']); $wallet = $john->wallets->first(); $packageDomain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $packageKolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $packageLite = \App\Package::withEnvTenantContext()->where('title', 'lite')->first(); $domain->assignPackage($packageDomain, $john); $john->assignPackage($packageKolab); $jack = User::create( [ 'email' => 'jack@kolab.org', 'password' => \App\Utils::generatePassphrase() ] ); $jack->setSettings( [ 'first_name' => 'Jack', 'last_name' => 'Daniels', 'currency' => 'USD', 'country' => 'US' ] ); $jack->setAliases(['jack.daniels@kolab.org']); $john->assignPackage($packageKolab, $jack); foreach ($john->entitlements as $entitlement) { $entitlement->created_at = Carbon::now()->subMonthsWithoutOverflow(1); $entitlement->updated_at = Carbon::now()->subMonthsWithoutOverflow(1); $entitlement->save(); } $ned = User::create( [ 'email' => 'ned@kolab.org', 'password' => \App\Utils::generatePassphrase() ] ); $ned->setSettings( [ 'first_name' => 'Edward', 'last_name' => 'Flanders', 'currency' => 'USD', 'country' => 'US' ] ); $john->assignPackage($packageKolab, $ned); $ned->assignSku(\App\Sku::withEnvTenantContext()->where('title', 'activesync')->first(), 1); // Ned is a controller on Jack's wallet $john->wallets()->first()->addController($ned); // Ned is also our 2FA test user $sku2fa = Sku::withEnvTenantContext()->where('title', '2fa')->first(); $ned->assignSku($sku2fa); try { SecondFactor::seed('ned@kolab.org'); } catch (\Exception $e) { // meh } $joe = User::create( [ 'email' => 'joe@kolab.org', 'password' => \App\Utils::generatePassphrase() ] ); $john->assignPackage($packageLite, $joe); //$john->assignSku(Sku::firstOrCreate(['title' => 'beta'])); //$john->assignSku(Sku::firstOrCreate(['title' => 'meet'])); $joe->setAliases(['joe.monster@kolab.org']); - // factory(User::class, 10)->create(); - $jeroen = User::create( [ 'email' => 'jeroen@jeroen.jeroen', 'password' => \App\Utils::generatePassphrase() ] ); $jeroen->role = 'admin'; $jeroen->save(); $reseller = User::create( [ 'email' => 'reseller@' . \config('app.domain'), 'password' => \App\Utils::generatePassphrase() ] ); $reseller->role = 'reseller'; $reseller->save(); $reseller->assignPackage($packageKolab); // for tenants that are not the configured tenant id $tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get(); foreach ($tenants as $tenant) { $domain = Domain::where('tenant_id', $tenant->id)->first(); $packageKolab = \App\Package::where( [ 'title' => 'kolab', 'tenant_id' => $tenant->id ] )->first(); if ($domain) { $reseller = User::create( [ 'email' => 'reseller@' . $domain->namespace, 'password' => \App\Utils::generatePassphrase() ] ); $reseller->role = 'reseller'; $reseller->tenant_id = $tenant->id; $reseller->save(); $reseller->assignPackage($packageKolab); $user = User::create( [ 'email' => 'user@' . $domain->namespace, 'password' => \App\Utils::generatePassphrase() ] ); $user->tenant_id = $tenant->id; $user->save(); $user->assignPackage($packageKolab); } } } } diff --git a/src/phpunit.xml b/src/phpunit.xml index e3d49429..1f3e446a 100644 --- a/src/phpunit.xml +++ b/src/phpunit.xml @@ -1,44 +1,44 @@ tests/Unit tests/Functional tests/Feature tests/Browser - - + + ./app - - + + - + diff --git a/src/tests/Browser.php b/src/tests/Browser.php index b69fdba4..2ee0a1fd 100644 --- a/src/tests/Browser.php +++ b/src/tests/Browser.php @@ -1,266 +1,267 @@ resolver->findOrFail($selector); $value = (string) $element->getAttribute($attribute); + $error = "No expected text in [$selector][$attribute]. Found: $value"; - Assert::assertRegExp($regexp, $value, "No expected text in [$selector][$attribute]. Found: $value"); + Assert::assertMatchesRegularExpression($regexp, $value, $error); return $this; } /** * Assert number of (visible) elements */ public function assertElementsCount($selector, $expected_count, $visible = true) { $elements = $this->elements($selector); $count = count($elements); if ($visible) { foreach ($elements as $element) { if (!$element->isDisplayed()) { $count--; } } } Assert::assertEquals($expected_count, $count, "Count of [$selector] elements is not $expected_count"); return $this; } /** * Assert Tip element content */ public function assertTip($selector, $content) { return $this->click($selector) ->withinBody(function ($browser) use ($content) { $browser->waitFor('div.tooltip .tooltip-inner') ->assertSeeIn('div.tooltip .tooltip-inner', $content); }) ->click($selector); } /** * Assert Toast element content (and close it) */ public function assertToast(string $type, string $message, $title = null) { return $this->withinBody(function ($browser) use ($type, $title, $message) { $browser->with(new Toast($type), function (Browser $browser) use ($title, $message) { $browser->assertToastTitle($title) ->assertToastMessage($message) ->closeToast(); }); }); } /** * Assert specified error page is displayed. */ public function assertErrorPage(int $error_code, string $hint = '') { $this->with(new Error($error_code, $hint), function ($browser) { // empty, assertions will be made by the Error component itself }); return $this; } /** * Assert that the given element has specified class assigned. */ public function assertHasClass($selector, $class_name) { $element = $this->resolver->findOrFail($selector); $classes = explode(' ', (string) $element->getAttribute('class')); Assert::assertContains($class_name, $classes, "[$selector] has no class '{$class_name}'"); return $this; } /** * Assert that the given element is readonly */ public function assertReadonly($selector) { $element = $this->resolver->findOrFail($selector); $value = $element->getAttribute('readonly'); Assert::assertTrue($value == 'true', "Element [$selector] is not readonly"); return $this; } /** * Assert that the given element is not readonly */ public function assertNotReadonly($selector) { $element = $this->resolver->findOrFail($selector); $value = $element->getAttribute('readonly'); Assert::assertTrue($value != 'true', "Element [$selector] is not readonly"); return $this; } /** * Assert that the given element contains specified text, * no matter it's displayed or not. */ public function assertText($selector, $text) { $element = $this->resolver->findOrFail($selector); if ($text === '') { Assert::assertTrue((string) $element->getText() === $text, "Element's text is not empty [$selector]"); } else { Assert::assertTrue(strpos($element->getText(), $text) !== false, "No expected text in [$selector]"); } return $this; } /** * Assert that the given element contains specified text, * no matter it's displayed or not - using a regular expression. */ public function assertTextRegExp($selector, $regexp) { $element = $this->resolver->findOrFail($selector); - Assert::assertRegExp($regexp, $element->getText(), "No expected text in [$selector]"); + Assert::assertMatchesRegularExpression($regexp, $element->getText(), "No expected text in [$selector]"); return $this; } /** * Remove all toast messages */ public function clearToasts() { $this->script("jQuery('.toast-container > *').remove()"); return $this; } /** * Wait until a button becomes enabled and click it */ public function clickWhenEnabled($selector) { return $this->waitFor($selector . ':not([disabled])')->click($selector); } /** * Check if in Phone mode */ public static function isPhone() { return getenv('TESTS_MODE') == 'phone'; } /** * Check if in Tablet mode */ public static function isTablet() { return getenv('TESTS_MODE') == 'tablet'; } /** * Check if in Desktop mode */ public static function isDesktop() { return !self::isPhone() && !self::isTablet(); } /** * Returns content of a downloaded file */ public function readDownloadedFile($filename, $sleep = 5) { $filename = __DIR__ . "/Browser/downloads/$filename"; // Give the browser a chance to finish download if (!file_exists($filename) && $sleep) { sleep($sleep); } Assert::assertFileExists($filename); return file_get_contents($filename); } /** * Removes downloaded file */ public function removeDownloadedFile($filename) { @unlink(__DIR__ . "/Browser/downloads/$filename"); return $this; } /** * Clears the input field and related vue v-model data. */ public function vueClear($selector) { if ($this->resolver->prefix != 'body') { $selector = $this->resolver->prefix . ' ' . $selector; } // The existing clear(), and type() with empty string do not work. // We have to clear the field and dispatch 'input' event programatically. $this->script( "var element = document.querySelector('$selector');" . "element.value = '';" . "element.dispatchEvent(new Event('input'))" ); return $this; } /** * Execute code within body context. * Useful to execute code that selects elements outside of a component context */ public function withinBody($callback) { if ($this->resolver->prefix != 'body') { $orig_prefix = $this->resolver->prefix; $this->resolver->prefix = 'body'; } call_user_func($callback, $this); if (isset($orig_prefix)) { $this->resolver->prefix = $orig_prefix; } return $this; } } diff --git a/src/tests/Feature/Controller/OpenViduTest.php b/src/tests/Feature/Controller/OpenViduTest.php index 0e82a06d..77c85732 100644 --- a/src/tests/Feature/Controller/OpenViduTest.php +++ b/src/tests/Feature/Controller/OpenViduTest.php @@ -1,781 +1,781 @@ clearMeetEntitlements(); $room = Room::where('name', 'john')->first(); $room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]); } public function tearDown(): void { $this->clearMeetEntitlements(); $room = Room::where('name', 'john')->first(); $room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]); parent::tearDown(); } /** * Test listing user rooms * * @group openvidu */ public function testIndex(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); Room::where('user_id', $jack->id)->delete(); // Unauth access not allowed $response = $this->get("api/v4/openvidu/rooms"); $response->assertStatus(401); // John has one room $response = $this->actingAs($john)->get("api/v4/openvidu/rooms"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(1, $json['count']); $this->assertCount(1, $json['list']); $this->assertSame('john', $json['list'][0]['name']); // Jack has no room, but it will be auto-created $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(1, $json['count']); $this->assertCount(1, $json['list']); - $this->assertRegExp('/^[0-9a-z-]{11}$/', $json['list'][0]['name']); + $this->assertMatchesRegularExpression('/^[0-9a-z-]{11}$/', $json['list'][0]['name']); } /** * Test joining the room * * @group openvidu */ public function testJoinRoom(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $room = Room::where('name', 'john')->first(); $room->session_id = null; $room->save(); $this->assignMeetEntitlement($john); // Unauth access, no session yet $response = $this->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(422); $json = $response->json(); $this->assertSame(323, $json['code']); // Non-existing room name $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing"); $response->assertStatus(404); // TODO: Test accessing an existing room of deleted owner // Non-owner, no session yet $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(422); $json = $response->json(); $this->assertSame(323, $json['code']); // Room owner, no session yet $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(422); $json = $response->json(); $this->assertSame(324, $json['code']); $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); $session_id = $room->fresh()->session_id; $this->assertSame(Room::ROLE_SUBSCRIBER | Room::ROLE_MODERATOR | Room::ROLE_OWNER, $json['role']); $this->assertSame($session_id, $json['session']); $this->assertTrue(is_string($session_id) && !empty($session_id)); $this->assertTrue(strpos($json['token'], 'wss://') === 0); $john_token = $json['token']; // Non-owner, now the session exists, no 'init' argument $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(422); $json = $response->json(); $this->assertSame(322, $json['code']); $this->assertTrue(empty($json['token'])); // Non-owner, now the session exists, with 'init', but no 'canPublish' argument $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); $this->assertSame(Room::ROLE_SUBSCRIBER, $json['role']); $this->assertSame($session_id, $json['session']); $this->assertTrue(strpos($json['token'], 'wss://') === 0); $this->assertTrue($json['token'] != $john_token); // Non-owner, now the session exists, with 'init', and with 'role=PUBLISHER' $post = ['canPublish' => true, 'init' => 1]; $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame(Room::ROLE_PUBLISHER, $json['role']); $this->assertSame($session_id, $json['session']); $this->assertTrue(strpos($json['token'], 'wss://') === 0); $this->assertTrue($json['token'] != $john_token); $this->assertEmpty($json['config']['password']); $this->assertEmpty($json['config']['requires_password']); // Non-owner, password protected room, password not provided $room->setSettings(['password' => 'pass']); $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(422); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(325, $json['code']); $this->assertSame('error', $json['status']); $this->assertSame('Failed to join the session. Invalid password.', $json['message']); $this->assertEmpty($json['config']['password']); $this->assertTrue($json['config']['requires_password']); // Non-owner, password protected room, invalid provided $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['password' => 'aa']); $response->assertStatus(422); $json = $response->json(); $this->assertSame(325, $json['code']); // Non-owner, password protected room, valid password provided // TODO: Test without init=1 $post = ['password' => 'pass', 'init' => 'init']; $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame($session_id, $json['session']); // Make sure the room owner can access the password protected room w/o password // TODO: Test without init=1 $post = ['init' => 'init']; $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(200); // Test 'nomedia' room option $room->setSettings(['nomedia' => 'true', 'password' => null]); $post = ['init' => 'init', 'canPublish' => true]; $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame(Room::ROLE_PUBLISHER & $json['role'], Room::ROLE_PUBLISHER); $post = ['init' => 'init', 'canPublish' => true]; $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame(Room::ROLE_PUBLISHER & $json['role'], 0); } /** * Test locked room and join requests * * @group openvidu */ public function testJoinRequests(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $room = Room::where('name', 'john')->first(); $room->session_id = null; $room->save(); $room->setSettings(['password' => null, 'locked' => 'true']); $this->assignMeetEntitlement($john); // Create the session (also makes sure the owner can access a locked room) $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); // Non-owner, locked room, invalid/missing input $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(422); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(326, $json['code']); $this->assertSame('error', $json['status']); $this->assertSame('Failed to join the session. Room locked.', $json['message']); $this->assertTrue($json['config']['locked']); // Non-owner, locked room, invalid requestId $post = ['nickname' => 'name', 'requestId' => '-----', 'init' => 1]; $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame(326, $json['code']); // Non-owner, locked room, invalid requestId $post = ['nickname' => 'name', 'init' => 1]; $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame(326, $json['code']); // Non-owner, locked room, valid input $reqId = '12345678'; $post = ['nickname' => 'name', 'requestId' => $reqId, 'picture' => 'data:image/png;base64,01234']; $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(327, $json['code']); $this->assertSame('error', $json['status']); $this->assertSame('Failed to join the session. Room locked.', $json['message']); $this->assertTrue($json['config']['locked']); // TODO: How do we assert that a signal has been sent to the owner? // Test denying a request // Unknown room $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/unknown/request/unknown/deny"); $response->assertStatus(404); // Unknown request Id $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/request/unknown/deny"); $response->assertStatus(500); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertSame('Failed to deny the join request.', $json['message']); // Non-owner access forbidden $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/request/{$reqId}/deny"); $response->assertStatus(403); // Valid request $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/request/{$reqId}/deny"); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); // Non-owner, locked room, join request denied $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame(327, $json['code']); // Test accepting a request // Unknown room $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/unknown/request/unknown/accept"); $response->assertStatus(404); // Unknown request Id $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/request/unknown/accept"); $response->assertStatus(500); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertSame('Failed to accept the join request.', $json['message']); // Non-owner access forbidden $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/request/{$reqId}/accept"); $response->assertStatus(403); // Valid request $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/request/{$reqId}/accept"); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); // Non-owner, locked room, join request accepted $post['init'] = 1; $post['canPublish'] = true; $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame(Room::ROLE_PUBLISHER, $json['role']); $this->assertTrue(strpos($json['token'], 'wss://') === 0); // TODO: Test a scenario where both password and lock are enabled // TODO: Test accepting/denying as a non-owner moderator } /** * Test joining the room * * @group openvidu * @depends testJoinRoom */ public function testJoinRoomGuest(): void { $this->assignMeetEntitlement('john@kolab.org'); // There's no asy way to logout the user in the same test after // using actingAs(). That's why this is moved to a separate test $room = Room::where('name', 'john')->first(); // Guest, request with screenShare token $post = ['canPublish' => true, 'screenShare' => 1, 'init' => 1]; $response = $this->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame(Room::ROLE_PUBLISHER, $json['role']); $this->assertSame($room->session_id, $json['session']); $this->assertTrue(strpos($json['token'], 'wss://') === 0); } /** * Test closing the room (session) * * @group openvidu * @depends testJoinRoom */ public function testCloseRoom(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $room = Room::where('name', 'john')->first(); // Unauth access not allowed $response = $this->post("api/v4/openvidu/rooms/{$room->name}/close", []); $response->assertStatus(401); // Non-existing room name $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing/close", []); $response->assertStatus(404); // Non-owner $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/close", []); $response->assertStatus(403); // Room owner $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/close", []); $response->assertStatus(200); $json = $response->json(); $this->assertNull($room->fresh()->session_id); $this->assertSame('success', $json['status']); $this->assertSame("The session has been closed successfully.", $json['message']); $this->assertCount(2, $json); // TODO: Test if the session is removed from the OpenVidu server too // Test error handling when it's not possible to delete the session on // the OpenVidu server (use fake session_id) $room->session_id = 'aaa'; $room->save(); $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/close", []); $response->assertStatus(500); $json = $response->json(); $this->assertSame('aaa', $room->fresh()->session_id); $this->assertSame('error', $json['status']); $this->assertSame("Failed to close the session.", $json['message']); $this->assertCount(2, $json); } /** * Test creating an extra connection for screen sharing * * @group openvidu */ public function testCreateConnection(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $room = Room::where('name', 'john')->first(); $room->session_id = null; $room->save(); $this->assignMeetEntitlement($john); // First we create the session $post = ['init' => 1, 'canPublish' => 1]; $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", $post); $response->assertStatus(200); $json = $response->json(); $owner_auth_token = $json['authToken']; // And the other user connection $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); $conn_id = $json['connectionId']; $auth_token = $json['authToken']; // Non-existing room name $response = $this->post("api/v4/openvidu/rooms/non-existing/connections", []); $response->assertStatus(404); // No connection token provided $response = $this->post("api/v4/openvidu/rooms/{$room->name}/connections", []); $response->assertStatus(403); // Invalid token $response = $this->actingAs($jack) ->withHeaders([OpenViduController::AUTH_HEADER => '123']) ->post("api/v4/openvidu/rooms/{$room->name}/connections", []); $response->assertStatus(403); // Subscriber can't get the screen-sharing connection // Note: We're acting as Jack because there's no easy way to unset the 'actingAs' user // throughout the test $response = $this->actingAs($jack) ->withHeaders([OpenViduController::AUTH_HEADER => $auth_token]) ->post("api/v4/openvidu/rooms/{$room->name}/connections", []); $response->assertStatus(403); // Publisher can get the connection $response = $this->actingAs($jack) ->withHeaders([OpenViduController::AUTH_HEADER => $owner_auth_token]) ->post("api/v4/openvidu/rooms/{$room->name}/connections", []); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertTrue(strpos($json['token'], 'wss://') === 0); // OpenVidu 2.18 does not send 'role' param in the token uri // $this->assertTrue(strpos($json['token'], 'role=PUBLISHER') !== false); } /** * Test dismissing a participant (closing a connection) * * @group openvidu */ public function testDismissConnection(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $room = Room::where('name', 'john')->first(); $room->session_id = null; $room->save(); $this->assignMeetEntitlement($john); // First we create the session $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); // And the other user connection $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); $conn_id = $json['connectionId']; $room->refresh(); $conn_data = $room->getOVConnection($conn_id); $this->assertSame($conn_id, $conn_data['connectionId']); // Non-existing room name $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing/connections/{$conn_id}/dismiss"); $response->assertStatus(404); // TODO: Test accessing an existing room of deleted owner // Non-existing connection $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/connections/123/dismiss"); $response->assertStatus(404); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertSame('The connection does not exist.', $json['message']); // Non-owner access $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}/dismiss"); $response->assertStatus(403); // Expected success $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}/dismiss"); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertNull($room->getOVConnection($conn_id)); // Test acting as a moderator $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); $conn_id = $json['connectionId']; // Note: We're acting as Jack because there's no easy way to unset a 'actingAs' user // throughout the test $response = $this->actingAs($jack) ->withHeaders([OpenViduController::AUTH_HEADER => $this->getModeratorToken($room)]) ->post("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}/dismiss"); $response->assertStatus(200); } /** * Test configuring the room (session) * * @group openvidu */ public function testSetRoomConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $room = Room::where('name', 'john')->first(); // Unauth access not allowed $response = $this->post("api/v4/openvidu/rooms/{$room->name}/config", []); $response->assertStatus(401); // Non-existing room name $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing/config", []); $response->assertStatus(404); // TODO: Test a room with a deleted owner // Non-owner $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/config", []); $response->assertStatus(403); // Room owner $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/config", []); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame("Room configuration updated successfully.", $json['message']); // Set password and room lock $post = ['password' => 'aaa', 'locked' => 1]; $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame("Room configuration updated successfully.", $json['message']); $room->refresh(); $this->assertSame('aaa', $room->getSetting('password')); $this->assertSame('true', $room->getSetting('locked')); // Unset password and room lock $post = ['password' => '', 'locked' => 0]; $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame("Room configuration updated successfully.", $json['message']); $room->refresh(); $this->assertSame(null, $room->getSetting('password')); $this->assertSame(null, $room->getSetting('locked')); // Test invalid option error $post = ['password' => 'eee', 'unknown' => 0]; $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertSame("Invalid room configuration option.", $json['errors']['unknown']); $room->refresh(); $this->assertSame(null, $room->getSetting('password')); } /** * Test updating a participant (connection) * * @group openvidu */ public function testUpdateConnection(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $room = Room::where('name', 'john')->first(); $room->session_id = null; $room->save(); $this->assignMeetEntitlement($john); // First we create the session $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); $owner_conn_id = $json['connectionId']; // And the other user connection $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); $conn_id = $json['connectionId']; $auth_token = $json['authToken']; $room->refresh(); $conn_data = $room->getOVConnection($conn_id); $this->assertSame($conn_id, $conn_data['connectionId']); // Non-existing room name $response = $this->actingAs($john)->put("api/v4/openvidu/rooms/non-existing/connections/{$conn_id}", []); $response->assertStatus(404); // Non-existing connection $response = $this->actingAs($john)->put("api/v4/openvidu/rooms/{$room->name}/connections/123", []); $response->assertStatus(404); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertSame('The connection does not exist.', $json['message']); // Non-owner access (empty post) $response = $this->actingAs($jack)->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", []); $response->assertStatus(200); // Non-owner access (role update) $post = ['role' => Room::ROLE_PUBLISHER | Room::ROLE_MODERATOR]; $response = $this->actingAs($jack)->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post); $response->assertStatus(403); // Expected success $post = ['role' => Room::ROLE_PUBLISHER | Room::ROLE_MODERATOR]; $response = $this->actingAs($john)->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame($post['role'], Connection::find($conn_id)->role); // Access as moderator // Note: We're acting as Jack because there's no easy way to unset a 'actingAs' user // throughout the test $token = $this->getModeratorToken($room); $post = ['role' => Room::ROLE_PUBLISHER]; $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $token]) ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame($post['role'], Connection::find($conn_id)->role); // Assert that it's not possible to add/remove the 'owner' role $post = ['role' => Room::ROLE_PUBLISHER | Room::ROLE_OWNER]; $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $token]) ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post); $response->assertStatus(403); $post = ['role' => Room::ROLE_PUBLISHER]; $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $token]) ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$owner_conn_id}", $post); $response->assertStatus(403); // Assert that removing a 'moderator' role from the owner is not possible $post = ['role' => Room::ROLE_PUBLISHER | Room::ROLE_OWNER]; $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $token]) ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$owner_conn_id}", $post); $response->assertStatus(200); $this->assertSame($post['role'] | Room::ROLE_MODERATOR, Connection::find($owner_conn_id)->role); // Assert that non-moderator token does not allow access $post = ['role' => Room::ROLE_SUBSCRIBER]; $response = $this->actingAs($jack)->withHeaders([OpenViduController::AUTH_HEADER => $auth_token]) ->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post); $response->assertStatus(403); // TODO: Test updating 'language' and 'hand' properties } /** * Create a moderator connection to the room session. * * @param \App\OpenVidu\Room $room The room * * @return string The connection authentication token */ private function getModeratorToken(Room $room): string { $result = $room->getSessionToken(Room::ROLE_MODERATOR); return $result['authToken']; } } diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php index e1863865..2ed0af63 100644 --- a/src/tests/Feature/Controller/PaymentsMollieTest.php +++ b/src/tests/Feature/Controller/PaymentsMollieTest.php @@ -1,1062 +1,1062 @@ 'mollie']); $john = $this->getTestUser('john@kolab.org'); $wallet = $john->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); $types = [ Transaction::WALLET_CREDIT, Transaction::WALLET_REFUND, Transaction::WALLET_CHARGEBACK, ]; Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { $john = $this->getTestUser('john@kolab.org'); $wallet = $john->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); $types = [ Transaction::WALLET_CREDIT, Transaction::WALLET_REFUND, Transaction::WALLET_CHARGEBACK, ]; Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete(); parent::tearDown(); } /** * Test creating/updating/deleting an outo-payment mandate * * @group mollie */ public function testMandates(): void { // Unauth access not allowed $response = $this->get("api/v4/payments/mandate"); $response->assertStatus(401); $response = $this->post("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->put("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->delete("api/v4/payments/mandate"); $response->assertStatus(401); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Test creating a mandate (invalid input) $post = []; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); // Test creating a mandate (invalid input) $post = ['amount' => 100, 'balance' => 'a']; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame('The balance must be a number.', $json['errors']['balance'][0]); // Test creating a mandate (amount smaller than the minimum value) $post = ['amount' => -100, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Test creating a mandate (negative balance, amount too small) Wallet::where('id', $wallet->id)->update(['balance' => -2000]); $post = ['amount' => PaymentProvider::MIN_AMOUNT / 100, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame("The specified amount does not cover the balance on the account.", $json['errors']['amount']); // Test creating a mandate (valid input) $post = ['amount' => 20.10, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); - $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']); + $this->assertMatchesRegularExpression('|^https://www.mollie.com|', $json['redirectUrl']); // Assert the proper payment amount has been used $payment = Payment::where('id', $json['id'])->first(); $this->assertSame(2010, $payment->amount); $this->assertSame($wallet->id, $payment->wallet_id); $this->assertSame(\config('app.name') . " Auto-Payment Setup", $payment->description); $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type); // Test fetching the mandate information $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertEquals('Credit Card', $json['method']); $this->assertSame(true, $json['isPending']); $this->assertSame(false, $json['isValid']); $this->assertSame(false, $json['isDisabled']); $mandate_id = $json['id']; // We would have to invoke a browser to accept the "first payment" to make // the mandate validated/completed. Instead, we'll mock the mandate object. $mollie_response = [ 'resource' => 'mandate', 'id' => $mandate_id, 'status' => 'valid', 'method' => 'creditcard', 'details' => [ 'cardNumber' => '4242', 'cardLabel' => 'Visa', ], 'customerId' => 'cst_GMfxGPt7Gj', 'createdAt' => '2020-04-28T11:09:47+00:00', ]; $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $wallet = $user->wallets()->first(); $wallet->setSetting('mandate_disabled', 1); $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertEquals('Visa (**** **** **** 4242)', $json['method']); $this->assertSame(false, $json['isPending']); $this->assertSame(true, $json['isValid']); $this->assertSame(true, $json['isDisabled']); Bus::fake(); $wallet->setSetting('mandate_disabled', null); $wallet->balance = 1000; $wallet->save(); // Test updating mandate details (invalid input) $post = []; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); $post = ['amount' => -100, 'balance' => 0]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Test updating a mandate (valid input) $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['amount' => 30.10, 'balance' => 10]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertSame($mandate_id, $json['id']); $this->assertFalse($json['isDisabled']); $wallet->refresh(); $this->assertEquals(30.10, $wallet->getSetting('mandate_amount')); $this->assertEquals(10, $wallet->getSetting('mandate_balance')); Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 0); // Test updating a disabled mandate (invalid input) $wallet->setSetting('mandate_disabled', 1); $wallet->balance = -2000; $wallet->save(); $user->refresh(); // required so the controller sees the wallet update from above $post = ['amount' => 15.10, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame('The specified amount does not cover the balance on the account.', $json['errors']['amount']); // Test updating a disabled mandate (valid input) $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['amount' => 30, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertSame($mandate_id, $json['id']); $this->assertFalse($json['isDisabled']); Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 1); Bus::assertDispatched(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { $job_wallet = $this->getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); $this->unmockMollie(); // Delete mandate $response = $this->actingAs($user)->delete("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been removed.', $json['message']); // Confirm with Mollie the mandate does not exist $customer_id = $wallet->getSetting('mollie_id'); $this->expectException(\Mollie\Api\Exceptions\ApiException::class); $this->expectExceptionMessageMatches('/410: Gone/'); $mandate = mollie()->mandates()->getForId($customer_id, $mandate_id); $this->assertNull($wallet->fresh()->getSetting('mollie_mandate_id')); // Test Mollie's "410 Gone" response handling when fetching the mandate info // It is expected to remove the mandate reference $mollie_response = [ 'status' => 410, 'title' => "Gone", 'detail' => "You are trying to access an object, which has previously been deleted", '_links' => [ 'documentation' => [ 'href' => "https://docs.mollie.com/errors", 'type' => "text/html" ] ] ]; $responseStack = $this->mockMollie(); $responseStack->append(new Response(410, [], json_encode($mollie_response))); $wallet->fresh()->setSetting('mollie_mandate_id', '123'); $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse(array_key_exists('id', $json)); $this->assertFalse(array_key_exists('method', $json)); $this->assertNull($wallet->fresh()->getSetting('mollie_mandate_id')); } /** * Test creating a payment and receiving a status via webhook * * @group mollie */ public function testStoreAndWebhook(): void { Bus::fake(); // Unauth access not allowed $response = $this->post("api/v4/payments", []); $response->assertStatus(401); // Invalid amount $user = $this->getTestUser('john@kolab.org'); $post = ['amount' => -1]; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Invalid currency $post = ['amount' => '12.34', 'currency' => 'FOO', 'methodId' => 'creditcard']; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(500); // Successful payment $post = ['amount' => '12.34', 'currency' => 'CHF', 'methodId' => 'creditcard']; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); - $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']); + $this->assertMatchesRegularExpression('|^https://www.mollie.com|', $json['redirectUrl']); $wallet = $user->wallets()->first(); $payments = Payment::where('wallet_id', $wallet->id)->get(); $this->assertCount(1, $payments); $payment = $payments[0]; $this->assertSame(1234, $payment->amount); $this->assertSame(1234, $payment->currency_amount); $this->assertSame('CHF', $payment->currency); $this->assertSame(\config('app.name') . ' Payment', $payment->description); $this->assertSame('open', $payment->status); $this->assertEquals(0, $wallet->balance); // Test the webhook // Note: Webhook end-point does not require authentication $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "paid", // Status is not enough, paidAt is used to distinguish the state "paidAt" => date('c'), "mode" => "test", ]; // We'll trigger the webhook with payment id and use mocking for // a request to the Mollie payments API. We cannot force Mollie // to make the payment status change. $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['id' => $payment->id]; $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); $transaction = $wallet->transactions() ->where('type', Transaction::WALLET_CREDIT)->get()->last(); $this->assertSame(1234, $transaction->amount); $this->assertSame( "Payment transaction {$payment->id} using Mollie", $transaction->description ); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); // Verify "paid -> open -> paid" scenario, assert that balance didn't change $mollie_response['status'] = 'open'; unset($mollie_response['paidAt']); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); $mollie_response['status'] = 'paid'; $mollie_response['paidAt'] = date('c'); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Test for payment failure Bus::fake(); $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "failed", "mode" => "test", ]; // We'll trigger the webhook with payment id and use mocking for // a request to the Mollie payments API. We cannot force Mollie // to make the payment status change. $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame('failed', $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); } /** * Test creating a payment and receiving a status via webhook using a foreign currency * * @group mollie */ public function testStoreAndWebhookForeignCurrency(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Successful payment in EUR $post = ['amount' => '12.34', 'currency' => 'EUR', 'methodId' => 'banktransfer']; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(200); $payment = $wallet->payments() ->where('currency', 'EUR')->get()->last(); $this->assertSame(1234, $payment->amount); $this->assertSame(1117, $payment->currency_amount); $this->assertSame('EUR', $payment->currency); $this->assertEquals(0, $wallet->balance); $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "paid", // Status is not enough, paidAt is used to distinguish the state "paidAt" => date('c'), "mode" => "test", ]; $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['id' => $payment->id]; $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); } /** * Test automatic payment charges * * @group mollie */ public function testTopUp(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Create a valid mandate first (balance=0, so there's no extra payment yet) $this->createMandate($wallet, ['amount' => 20.10, 'balance' => 0]); $wallet->setSetting('mandate_balance', 10); // Expect a recurring payment as we have a valid mandate at this point // and the balance is below the threshold $result = PaymentsController::topUpWallet($wallet); $this->assertTrue($result); // Check that the payments table contains a new record with proper amount. // There should be two records, one for the mandate payment and another for // the top-up payment $payments = $wallet->payments()->orderBy('amount')->get(); $this->assertCount(2, $payments); $this->assertSame(0, $payments[0]->amount); $this->assertSame(0, $payments[0]->currency_amount); $this->assertSame(2010, $payments[1]->amount); $this->assertSame(2010, $payments[1]->currency_amount); $payment = $payments[1]; // In mollie we don't have to wait for a webhook, the response to // PaymentIntent already sets the status to 'paid', so we can test // immediately the balance update // Assert that email notification job has been dispatched $this->assertSame(PaymentProvider::STATUS_PAID, $payment->status); $this->assertEquals(2010, $wallet->fresh()->balance); $transaction = $wallet->transactions() ->where('type', Transaction::WALLET_CREDIT)->get()->last(); $this->assertSame(2010, $transaction->amount); $this->assertSame( "Auto-payment transaction {$payment->id} using Mastercard (**** **** **** 6787)", $transaction->description ); Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); // Expect no payment if the mandate is disabled $wallet->setSetting('mandate_disabled', 1); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(2, $wallet->payments()->get()); // Expect no payment if balance is ok $wallet->setSetting('mandate_disabled', null); $wallet->balance = 1000; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(2, $wallet->payments()->get()); // Expect no payment if the top-up amount is not enough $wallet->setSetting('mandate_disabled', null); $wallet->balance = -2050; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(2, $wallet->payments()->get()); Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) { $job_wallet = $this->getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); // Expect no payment if there's no mandate $wallet->setSetting('mollie_mandate_id', null); $wallet->balance = 0; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(2, $wallet->payments()->get()); Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); // Test webhook for recurring payments $wallet->transactions()->delete(); $responseStack = $this->mockMollie(); Bus::fake(); $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "paid", // Status is not enough, paidAt is used to distinguish the state "paidAt" => date('c'), "mode" => "test", ]; // We'll trigger the webhook with payment id and use mocking for // a request to the Mollie payments API. We cannot force Mollie // to make the payment status change. $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['id' => $payment->id]; $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(2010, $wallet->fresh()->balance); $transaction = $wallet->transactions() ->where('type', Transaction::WALLET_CREDIT)->get()->last(); $this->assertSame(2010, $transaction->amount); $this->assertSame( "Auto-payment transaction {$payment->id} using Mollie", $transaction->description ); // Assert that email notification job has been dispatched Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); Bus::fake(); // Test for payment failure $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $wallet->setSetting('mollie_mandate_id', 'xxx'); $wallet->setSetting('mandate_disabled', null); $mollie_response = [ "resource" => "payment", "id" => $payment->id, "status" => "failed", "mode" => "test", ]; $responseStack->append(new Response(200, [], json_encode($mollie_response))); $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $wallet->refresh(); $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); $this->assertEquals(2010, $wallet->balance); $this->assertTrue(!empty($wallet->getSetting('mandate_disabled'))); // Assert that email notification job has been dispatched Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); $this->unmockMollie(); } /** * Test refund/chargeback handling by the webhook * * @group mollie */ public function testRefundAndChargeback(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->transactions()->delete(); $mollie = PaymentProvider::factory('mollie'); // Create a paid payment $payment = Payment::create([ 'id' => 'tr_123456', 'status' => PaymentProvider::STATUS_PAID, 'amount' => 123, 'currency_amount' => 123, 'currency' => 'CHF', 'type' => PaymentProvider::TYPE_ONEOFF, 'wallet_id' => $wallet->id, 'provider' => 'mollie', 'description' => 'test', ]); // Test handling a refund by the webhook $mollie_response1 = [ "resource" => "payment", "id" => $payment->id, "status" => "paid", // Status is not enough, paidAt is used to distinguish the state "paidAt" => date('c'), "mode" => "test", "_links" => [ "refunds" => [ "href" => "https://api.mollie.com/v2/payments/{$payment->id}/refunds", "type" => "application/hal+json" ] ] ]; $mollie_response2 = [ "count" => 1, "_links" => [], "_embedded" => [ "refunds" => [ [ "resource" => "refund", "id" => "re_123456", "status" => \Mollie\Api\Types\RefundStatus::STATUS_REFUNDED, "paymentId" => $payment->id, "description" => "refund desc", "amount" => [ "currency" => "CHF", "value" => "1.01", ], ] ] ] ]; // We'll trigger the webhook with payment id and use mocking for // requests to the Mollie payments API. $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response1))); $responseStack->append(new Response(200, [], json_encode($mollie_response2))); $post = ['id' => $payment->id]; $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $wallet->refresh(); $this->assertEquals(-101, $wallet->balance); $transactions = $wallet->transactions()->where('type', Transaction::WALLET_REFUND)->get(); $this->assertCount(1, $transactions); $this->assertSame(-101, $transactions[0]->amount); $this->assertSame(Transaction::WALLET_REFUND, $transactions[0]->type); $this->assertSame("refund desc", $transactions[0]->description); $payments = $wallet->payments()->where('id', 're_123456')->get(); $this->assertCount(1, $payments); $this->assertSame(-101, $payments[0]->amount); $this->assertSame(-101, $payments[0]->currency_amount); $this->assertSame(PaymentProvider::STATUS_PAID, $payments[0]->status); $this->assertSame(PaymentProvider::TYPE_REFUND, $payments[0]->type); $this->assertSame("mollie", $payments[0]->provider); $this->assertSame("refund desc", $payments[0]->description); // Test handling a chargeback by the webhook $mollie_response1["_links"] = [ "chargebacks" => [ "href" => "https://api.mollie.com/v2/payments/{$payment->id}/chargebacks", "type" => "application/hal+json" ] ]; $mollie_response2 = [ "count" => 1, "_links" => [], "_embedded" => [ "chargebacks" => [ [ "resource" => "chargeback", "id" => "chb_123456", "paymentId" => $payment->id, "amount" => [ "currency" => "CHF", "value" => "0.15", ], ] ] ] ]; // We'll trigger the webhook with payment id and use mocking for // requests to the Mollie payments API. $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response1))); $responseStack->append(new Response(200, [], json_encode($mollie_response2))); $post = ['id' => $payment->id]; $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $wallet->refresh(); $this->assertEquals(-116, $wallet->balance); $transactions = $wallet->transactions()->where('type', Transaction::WALLET_CHARGEBACK)->get(); $this->assertCount(1, $transactions); $this->assertSame(-15, $transactions[0]->amount); $this->assertSame(Transaction::WALLET_CHARGEBACK, $transactions[0]->type); $this->assertSame('', $transactions[0]->description); $payments = $wallet->payments()->where('id', 'chb_123456')->get(); $this->assertCount(1, $payments); $this->assertSame(-15, $payments[0]->amount); $this->assertSame(PaymentProvider::STATUS_PAID, $payments[0]->status); $this->assertSame(PaymentProvider::TYPE_CHARGEBACK, $payments[0]->type); $this->assertSame("mollie", $payments[0]->provider); $this->assertSame('', $payments[0]->description); Bus::assertNotDispatched(\App\Jobs\PaymentEmail::class); $this->unmockMollie(); } /** * Test refund/chargeback handling by the webhook in a foreign currency * * @group mollie */ public function testRefundAndChargebackForeignCurrency(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->transactions()->delete(); $mollie = PaymentProvider::factory('mollie'); // Create a paid payment $payment = Payment::create([ 'id' => 'tr_123456', 'status' => PaymentProvider::STATUS_PAID, 'amount' => 1234, 'currency_amount' => 1117, 'currency' => 'EUR', 'type' => PaymentProvider::TYPE_ONEOFF, 'wallet_id' => $wallet->id, 'provider' => 'mollie', 'description' => 'test', ]); // Test handling a refund by the webhook $mollie_response1 = [ "resource" => "payment", "id" => $payment->id, "status" => "paid", // Status is not enough, paidAt is used to distinguish the state "paidAt" => date('c'), "mode" => "test", "_links" => [ "refunds" => [ "href" => "https://api.mollie.com/v2/payments/{$payment->id}/refunds", "type" => "application/hal+json" ] ] ]; $mollie_response2 = [ "count" => 1, "_links" => [], "_embedded" => [ "refunds" => [ [ "resource" => "refund", "id" => "re_123456", "status" => \Mollie\Api\Types\RefundStatus::STATUS_REFUNDED, "paymentId" => $payment->id, "description" => "refund desc", "amount" => [ "currency" => "EUR", "value" => "1.01", ], ] ] ] ]; // We'll trigger the webhook with payment id and use mocking for // requests to the Mollie payments API. $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response1))); $responseStack->append(new Response(200, [], json_encode($mollie_response2))); $post = ['id' => $payment->id]; $response = $this->post("api/webhooks/payment/mollie", $post); $response->assertStatus(200); $wallet->refresh(); $this->assertTrue($wallet->balance <= -108); $this->assertTrue($wallet->balance >= -114); $payments = $wallet->payments()->where('id', 're_123456')->get(); $this->assertCount(1, $payments); $this->assertTrue($payments[0]->amount <= -108); $this->assertTrue($payments[0]->amount >= -114); $this->assertSame(-101, $payments[0]->currency_amount); $this->assertSame('EUR', $payments[0]->currency); $this->unmockMollie(); } /** * Create Mollie's auto-payment mandate using our API and Chrome browser */ protected function createMandate(Wallet $wallet, array $params) { // Use the API to create a first payment with a mandate $response = $this->actingAs($wallet->owner)->post("api/v4/payments/mandate", $params); $response->assertStatus(200); $json = $response->json(); // There's no easy way to confirm a created mandate. // The only way seems to be to fire up Chrome on checkout page // and do actions with use of Dusk browser. $this->startBrowser() ->visit($json['redirectUrl']) ->click('input[value="paid"]') ->click('button.form__button'); $this->stopBrowser(); } /** * Test listing a pending payment * * @group mollie */ public function testListingPayments(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); //Empty response $response = $this->actingAs($user)->get("api/v4/payments/pending"); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame(0, $json['count']); $this->assertSame(1, $json['page']); $this->assertSame(false, $json['hasMore']); $this->assertCount(0, $json['list']); $response = $this->actingAs($user)->get("api/v4/payments/has-pending"); $json = $response->json(); $this->assertSame(false, $json['hasPending']); $wallet = $user->wallets()->first(); // Successful payment $post = ['amount' => '12.34', 'currency' => 'CHF', 'methodId' => 'creditcard']; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(200); //A response $response = $this->actingAs($user)->get("api/v4/payments/pending"); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame(1, $json['count']); $this->assertSame(1, $json['page']); $this->assertSame(false, $json['hasMore']); $this->assertCount(1, $json['list']); $this->assertSame(PaymentProvider::STATUS_OPEN, $json['list'][0]['status']); $response = $this->actingAs($user)->get("api/v4/payments/has-pending"); $json = $response->json(); $this->assertSame(true, $json['hasPending']); // Set the payment to paid $payments = Payment::where('wallet_id', $wallet->id)->get(); $this->assertCount(1, $payments); $payment = $payments[0]; $payment->status = PaymentProvider::STATUS_PAID; $payment->save(); // They payment should be gone from the pending list now $response = $this->actingAs($user)->get("api/v4/payments/pending"); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame(0, $json['count']); $this->assertCount(0, $json['list']); $response = $this->actingAs($user)->get("api/v4/payments/has-pending"); $json = $response->json(); $this->assertSame(false, $json['hasPending']); } /** * Test listing payment methods * * @group mollie */ public function testListingPaymentMethods(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_ONEOFF); $response->assertStatus(200); $json = $response->json(); $this->assertCount(3, $json); $this->assertSame('creditcard', $json[0]['id']); $this->assertSame('paypal', $json[1]['id']); $this->assertSame('banktransfer', $json[2]['id']); $response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_RECURRING); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSame('creditcard', $json[0]['id']); } } diff --git a/src/tests/Feature/Controller/PaymentsStripeTest.php b/src/tests/Feature/Controller/PaymentsStripeTest.php index e9f70c5d..6114c994 100644 --- a/src/tests/Feature/Controller/PaymentsStripeTest.php +++ b/src/tests/Feature/Controller/PaymentsStripeTest.php @@ -1,743 +1,743 @@ 'stripe']); $john = $this->getTestUser('john@kolab.org'); $wallet = $john->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); Transaction::where('object_id', $wallet->id) ->where('type', Transaction::WALLET_CREDIT)->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { $john = $this->getTestUser('john@kolab.org'); $wallet = $john->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); Transaction::where('object_id', $wallet->id) ->where('type', Transaction::WALLET_CREDIT)->delete(); parent::tearDown(); } /** * Test creating/updating/deleting an outo-payment mandate * * @group stripe */ public function testMandates(): void { Bus::fake(); // Unauth access not allowed $response = $this->get("api/v4/payments/mandate"); $response->assertStatus(401); $response = $this->post("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->put("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->delete("api/v4/payments/mandate"); $response->assertStatus(401); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Test creating a mandate (invalid input) $post = []; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); // Test creating a mandate (invalid input) $post = ['amount' => 100, 'balance' => 'a']; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame('The balance must be a number.', $json['errors']['balance'][0]); // Test creating a mandate (invalid input) $post = ['amount' => -100, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Test creating a mandate (negative balance, amount too small) Wallet::where('id', $wallet->id)->update(['balance' => -2000]); $post = ['amount' => PaymentProvider::MIN_AMOUNT / 100, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame("The specified amount does not cover the balance on the account.", $json['errors']['amount']); // Test creating a mandate (valid input) $post = ['amount' => 20.10, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); - $this->assertRegExp('|^cs_test_|', $json['id']); + $this->assertMatchesRegularExpression('|^cs_test_|', $json['id']); // Assert the proper payment amount has been used // Stripe in 'setup' mode does not allow to set the amount $payment = Payment::where('wallet_id', $wallet->id)->first(); $this->assertSame(0, $payment->amount); $this->assertSame(\config('app.name') . " Auto-Payment Setup", $payment->description); $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type); // Test fetching the mandate information $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertSame(false, $json['isDisabled']); // We would have to invoke a browser to accept the "first payment" to make // the mandate validated/completed. Instead, we'll mock the mandate object. $setupIntent = '{ "id": "AAA", "object": "setup_intent", "created": 123456789, "payment_method": "pm_YYY", "status": "succeeded", "usage": "off_session", "customer": null }'; $paymentMethod = '{ "id": "pm_YYY", "object": "payment_method", "card": { "brand": "visa", "country": "US", "last4": "4242" }, "created": 123456789, "type": "card" }'; $client = $this->mockStripe(); $client->addResponse($setupIntent); $client->addResponse($paymentMethod); // As we do not use checkout page, we do not receive a webworker request // I.e. we have to fake the mandate id $wallet = $user->wallets()->first(); $wallet->setSetting('stripe_mandate_id', 'AAA'); $wallet->setSetting('mandate_disabled', 1); $response = $this->actingAs($user)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertEquals('Visa (**** **** **** 4242)', $json['method']); $this->assertSame(false, $json['isPending']); $this->assertSame(true, $json['isValid']); $this->assertSame(true, $json['isDisabled']); // Test updating mandate details (invalid input) $wallet->setSetting('mandate_disabled', null); $wallet->balance = 1000; $wallet->save(); $user->refresh(); $post = []; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertSame('The amount field is required.', $json['errors']['amount'][0]); $this->assertSame('The balance field is required.', $json['errors']['balance'][0]); $post = ['amount' => -100, 'balance' => 0]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Test updating a mandate (valid input) $client->addResponse($setupIntent); $client->addResponse($paymentMethod); $post = ['amount' => 30.10, 'balance' => 10]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertEquals(30.10, $wallet->getSetting('mandate_amount')); $this->assertEquals(10, $wallet->getSetting('mandate_balance')); $this->assertSame('AAA', $json['id']); $this->assertFalse($json['isDisabled']); // Test updating a disabled mandate (invalid input) $wallet->setSetting('mandate_disabled', 1); $wallet->balance = -2000; $wallet->save(); $user->refresh(); // required so the controller sees the wallet update from above $post = ['amount' => 15.10, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame('The specified amount does not cover the balance on the account.', $json['errors']['amount']); // Test updating a disabled mandate (valid input) $client->addResponse($setupIntent); $client->addResponse($paymentMethod); $post = ['amount' => 30, 'balance' => 1]; $response = $this->actingAs($user)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertSame('AAA', $json['id']); $this->assertFalse($json['isDisabled']); Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 1); Bus::assertDispatched(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { $job_wallet = $this->getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); $this->unmockStripe(); // TODO: Delete mandate } /** * Test creating a payment and receiving a status via webhook * * @group stripe */ public function testStoreAndWebhook(): void { Bus::fake(); // Unauth access not allowed $response = $this->post("api/v4/payments", []); $response->assertStatus(401); $user = $this->getTestUser('john@kolab.org'); $post = ['amount' => -1]; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; $this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']); // Invalid currency $post = ['amount' => '12.34', 'currency' => 'FOO', 'methodId' => 'creditcard']; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(500); // Successful payment $post = ['amount' => '12.34', 'currency' => 'CHF', 'methodId' => 'creditcard']; $response = $this->actingAs($user)->post("api/v4/payments", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); - $this->assertRegExp('|^cs_test_|', $json['id']); + $this->assertMatchesRegularExpression('|^cs_test_|', $json['id']); $wallet = $user->wallets()->first(); $payments = Payment::where('wallet_id', $wallet->id)->get(); $this->assertCount(1, $payments); $payment = $payments[0]; $this->assertSame(1234, $payment->amount); $this->assertSame(\config('app.name') . ' Payment', $payment->description); $this->assertSame('open', $payment->status); $this->assertEquals(0, $wallet->balance); // Test the webhook $post = [ 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", 'object' => "event", 'api_version' => "2020-03-02", 'created' => 1590147209, 'data' => [ 'object' => [ 'id' => $payment->id, 'object' => "payment_intent", 'amount' => 1234, 'amount_capturable' => 0, 'amount_received' => 1234, 'capture_method' => "automatic", 'client_secret' => "pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48", 'confirmation_method' => "automatic", 'created' => 1590147204, 'currency' => "chf", 'customer' => "cus_HKDZ53OsKdlM83", 'last_payment_error' => null, 'livemode' => false, 'metadata' => [], 'receipt_email' => "payment-test@kolabnow.com", 'status' => "succeeded" ] ], 'type' => "payment_intent.succeeded" ]; // Test payment succeeded event $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); $transaction = $wallet->transactions() ->where('type', Transaction::WALLET_CREDIT)->get()->last(); $this->assertSame(1234, $transaction->amount); $this->assertSame( "Payment transaction {$payment->id} using Stripe", $transaction->description ); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); // Test that balance didn't change if the same event is posted $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Test for payment failure ('failed' status) $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $post['type'] = "payment_intent.payment_failed"; $post['data']['object']['status'] = 'failed'; $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); // Test for payment failure ('canceled' status) $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $post['type'] = "payment_intent.canceled"; $post['data']['object']['status'] = 'canceled'; $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_CANCELED, $payment->fresh()->status); $this->assertEquals(1234, $wallet->fresh()->balance); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); } /** * Test receiving webhook request for setup intent * * @group stripe */ public function testCreateMandateAndWebhook(): void { $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); Wallet::where('id', $wallet->id)->update(['balance' => -1000]); // Test creating a mandate (valid input) $post = ['amount' => 20.10, 'balance' => 0]; $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post); $response->assertStatus(200); $payment = $wallet->payments()->first(); $this->assertSame(PaymentProvider::STATUS_OPEN, $payment->status); $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type); $this->assertSame(0, $payment->amount); $post = [ 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", 'object' => "event", 'api_version' => "2020-03-02", 'created' => 1590147209, 'data' => [ 'object' => [ 'id' => $payment->id, 'object' => "setup_intent", 'client_secret' => "pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48", 'created' => 1590147204, 'customer' => "cus_HKDZ53OsKdlM83", 'last_setup_error' => null, 'metadata' => [], 'status' => "succeeded" ] ], 'type' => "setup_intent.succeeded" ]; Bus::fake(); // Test payment succeeded event $response = $this->webhookRequest($post); $response->assertStatus(200); $payment->refresh(); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->status); $this->assertSame($payment->id, $wallet->fresh()->getSetting('stripe_mandate_id')); // Expect a WalletCharge job if the balance is negative Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 1); Bus::assertDispatched(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) { $job_wallet = TestCase::getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); // TODO: test other setup_intent.* events } /** * Test automatic payment charges * * @group stripe */ public function testTopUpAndWebhook(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Stripe API does not allow us to create a mandate easily // That's why we we'll mock API responses // Create a fake mandate $wallet->setSettings([ 'mandate_amount' => 20.10, 'mandate_balance' => 10, 'stripe_mandate_id' => 'AAA', ]); $setupIntent = json_encode([ "id" => "AAA", "object" => "setup_intent", "created" => 123456789, "payment_method" => "pm_YYY", "status" => "succeeded", "usage" => "off_session", "customer" => null ]); $paymentMethod = json_encode([ "id" => "pm_YYY", "object" => "payment_method", "card" => [ "brand" => "visa", "country" => "US", "last4" => "4242" ], "created" => 123456789, "type" => "card" ]); $paymentIntent = json_encode([ "id" => "pi_XX", "object" => "payment_intent", "created" => 123456789, "amount" => 2010, "currency" => "chf", "description" => "Kolab Recurring Payment" ]); $client = $this->mockStripe(); $client->addResponse($setupIntent); $client->addResponse($paymentMethod); $client->addResponse($setupIntent); $client->addResponse($paymentIntent); $client->addResponse($setupIntent); $client->addResponse($paymentMethod); // Expect a recurring payment as we have a valid mandate at this point $result = PaymentsController::topUpWallet($wallet); $this->assertTrue($result); // Check that the payments table contains a new record with proper amount // There should be two records, one for the first payment and another for // the recurring payment $this->assertCount(1, $wallet->payments()->get()); $payment = $wallet->payments()->first(); $this->assertSame(2010, $payment->amount); $this->assertSame(\config('app.name') . " Recurring Payment", $payment->description); $this->assertSame("pi_XX", $payment->id); // Expect no payment if the mandate is disabled $wallet->setSetting('mandate_disabled', 1); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); // Expect no payment if balance is ok $wallet->setSetting('mandate_disabled', null); $wallet->balance = 1000; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); // Expect no payment if the top-up amount is not enough $wallet->setSetting('mandate_disabled', null); $wallet->balance = -2050; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) { $job_wallet = $this->getObjectProperty($job, 'wallet'); return $job_wallet->id === $wallet->id; }); // Expect no payment if there's no mandate $wallet->setSetting('mollie_mandate_id', null); $wallet->balance = 0; $wallet->save(); $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1); $this->unmockStripe(); // Test webhook $post = [ 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa", 'object' => "event", 'api_version' => "2020-03-02", 'created' => 1590147209, 'data' => [ 'object' => [ 'id' => $payment->id, 'object' => "payment_intent", 'amount' => 2010, 'capture_method' => "automatic", 'created' => 1590147204, 'currency' => "chf", 'customer' => "cus_HKDZ53OsKdlM83", 'last_payment_error' => null, 'metadata' => [], 'receipt_email' => "payment-test@kolabnow.com", 'status' => "succeeded" ] ], 'type' => "payment_intent.succeeded" ]; // Test payment succeeded event $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status); $this->assertEquals(2010, $wallet->fresh()->balance); $transaction = $wallet->transactions() ->where('type', Transaction::WALLET_CREDIT)->get()->last(); $this->assertSame(2010, $transaction->amount); $this->assertSame( "Auto-payment transaction {$payment->id} using Stripe", $transaction->description ); // Assert that email notification job has been dispatched Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); Bus::fake(); // Test for payment failure ('failed' status) $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $wallet->setSetting('mandate_disabled', null); $post['type'] = "payment_intent.payment_failed"; $post['data']['object']['status'] = 'failed'; $response = $this->webhookRequest($post); $response->assertStatus(200); $wallet->refresh(); $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status); $this->assertEquals(2010, $wallet->balance); $this->assertTrue(!empty($wallet->getSetting('mandate_disabled'))); // Assert that email notification job has been dispatched Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1); Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) { $job_payment = $this->getObjectProperty($job, 'payment'); return $job_payment->id === $payment->id; }); Bus::fake(); // Test for payment failure ('canceled' status) $payment->refresh(); $payment->status = PaymentProvider::STATUS_OPEN; $payment->save(); $post['type'] = "payment_intent.canceled"; $post['data']['object']['status'] = 'canceled'; $response = $this->webhookRequest($post); $response->assertStatus(200); $this->assertSame(PaymentProvider::STATUS_CANCELED, $payment->fresh()->status); $this->assertEquals(2010, $wallet->fresh()->balance); // Assert that email notification job wasn't dispatched, // it is expected only for recurring payments Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0); } /** * Generate Stripe-Signature header for a webhook payload */ protected function webhookRequest($post) { $secret = \config('services.stripe.webhook_secret'); $ts = time(); $payload = "$ts." . json_encode($post); $sig = sprintf('t=%d,v1=%s', $ts, \hash_hmac('sha256', $payload, $secret)); return $this->withHeaders(['Stripe-Signature' => $sig]) ->json('POST', "api/webhooks/payment/stripe", $post); } /** * Test listing payment methods * * @group stripe */ public function testListingPaymentMethods(): void { Bus::fake(); $user = $this->getTestUser('john@kolab.org'); $response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_ONEOFF); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('creditcard', $json[0]['id']); $this->assertSame('paypal', $json[1]['id']); $response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_RECURRING); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSame('creditcard', $json[0]['id']); } } diff --git a/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php b/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php index 2a976976..f10b582f 100644 --- a/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php +++ b/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php @@ -1,258 +1,258 @@ 'mollie']); $reseller = $this->getTestUser('reseller@' . \config('app.domain')); $wallet = $reseller->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); Transaction::where('object_id', $wallet->id)->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { $reseller = $this->getTestUser('reseller@' . \config('app.domain')); $wallet = $reseller->wallets()->first(); Payment::where('wallet_id', $wallet->id)->delete(); Wallet::where('id', $wallet->id)->update(['balance' => 0]); WalletSetting::where('wallet_id', $wallet->id)->delete(); Transaction::where('object_id', $wallet->id)->delete(); parent::tearDown(); } /** * Test creating/updating/deleting an outo-payment mandate * * @group mollie */ public function testMandates(): void { // Unauth access not allowed $response = $this->get("api/v4/payments/mandate"); $response->assertStatus(401); $response = $this->post("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->put("api/v4/payments/mandate", []); $response->assertStatus(401); $response = $this->delete("api/v4/payments/mandate"); $response->assertStatus(401); $reseller = $this->getTestUser('reseller@' . \config('app.domain')); $wallet = $reseller->wallets()->first(); $wallet->balance = -10; $wallet->save(); // Test creating a mandate (valid input) $post = ['amount' => 20.10, 'balance' => 0]; $response = $this->actingAs($reseller)->post("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); - $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']); + $this->assertMatchesRegularExpression('|^https://www.mollie.com|', $json['redirectUrl']); // Assert the proper payment amount has been used $payment = Payment::where('id', $json['id'])->first(); $this->assertSame(2010, $payment->amount); $this->assertSame($wallet->id, $payment->wallet_id); $this->assertSame(\config('app.name') . " Auto-Payment Setup", $payment->description); $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type); // Test fetching the mandate information $response = $this->actingAs($reseller)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertEquals('Credit Card', $json['method']); $this->assertSame(true, $json['isPending']); $this->assertSame(false, $json['isValid']); $this->assertSame(false, $json['isDisabled']); $mandate_id = $json['id']; // We would have to invoke a browser to accept the "first payment" to make // the mandate validated/completed. Instead, we'll mock the mandate object. $mollie_response = [ 'resource' => 'mandate', 'id' => $mandate_id, 'status' => 'valid', 'method' => 'creditcard', 'details' => [ 'cardNumber' => '4242', 'cardLabel' => 'Visa', ], 'customerId' => 'cst_GMfxGPt7Gj', 'createdAt' => '2020-04-28T11:09:47+00:00', ]; $responseStack = $this->mockMollie(); $responseStack->append(new Response(200, [], json_encode($mollie_response))); $wallet = $reseller->wallets()->first(); $wallet->setSetting('mandate_disabled', 1); $response = $this->actingAs($reseller)->get("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals(20.10, $json['amount']); $this->assertEquals(0, $json['balance']); $this->assertEquals('Visa (**** **** **** 4242)', $json['method']); $this->assertSame(false, $json['isPending']); $this->assertSame(true, $json['isValid']); $this->assertSame(true, $json['isDisabled']); Bus::fake(); $wallet->setSetting('mandate_disabled', null); $wallet->balance = 1000; $wallet->save(); // Test updating a mandate (valid input) $responseStack->append(new Response(200, [], json_encode($mollie_response))); $post = ['amount' => 30.10, 'balance' => 10]; $response = $this->actingAs($reseller)->put("api/v4/payments/mandate", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been updated.', $json['message']); $this->assertSame($mandate_id, $json['id']); $this->assertFalse($json['isDisabled']); $wallet->refresh(); $this->assertEquals(30.10, $wallet->getSetting('mandate_amount')); $this->assertEquals(10, $wallet->getSetting('mandate_balance')); Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 0); $this->unmockMollie(); // Delete mandate $response = $this->actingAs($reseller)->delete("api/v4/payments/mandate"); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame('The auto-payment has been removed.', $json['message']); } /** * Test creating a payment * * @group mollie */ public function testStore(): void { Bus::fake(); // Unauth access not allowed $response = $this->post("api/v4/payments", []); $response->assertStatus(401); $reseller = $this->getTestUser('reseller@' . \config('app.domain')); // Successful payment $post = ['amount' => '12.34', 'currency' => 'CHF', 'methodId' => 'creditcard']; $response = $this->actingAs($reseller)->post("api/v4/payments", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); - $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']); + $this->assertMatchesRegularExpression('|^https://www.mollie.com|', $json['redirectUrl']); } /** * Test listing a pending payment * * @group mollie */ public function testListingPayments(): void { Bus::fake(); $reseller = $this->getTestUser('reseller@' . \config('app.domain')); // Empty response $response = $this->actingAs($reseller)->get("api/v4/payments/pending"); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame(0, $json['count']); $this->assertSame(1, $json['page']); $this->assertSame(false, $json['hasMore']); $this->assertCount(0, $json['list']); $response = $this->actingAs($reseller)->get("api/v4/payments/has-pending"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(false, $json['hasPending']); } /** * Test listing payment methods * * @group mollie */ public function testListingPaymentMethods(): void { Bus::fake(); $reseller = $this->getTestUser('reseller@' . \config('app.domain')); $response = $this->actingAs($reseller)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_ONEOFF); $response->assertStatus(200); $json = $response->json(); $this->assertCount(3, $json); $this->assertSame('creditcard', $json[0]['id']); $this->assertSame('paypal', $json[1]['id']); $this->assertSame('banktransfer', $json[2]['id']); } } diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php index 962716ce..0fc1766d 100644 --- a/src/tests/Feature/Controller/WalletsTest.php +++ b/src/tests/Feature/Controller/WalletsTest.php @@ -1,355 +1,355 @@ deleteTestUser('wallets-controller@kolabnow.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestUser('wallets-controller@kolabnow.com'); parent::tearDown(); } /** * Test for getWalletNotice() method */ public function testGetWalletNotice(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); $package = \App\Package::where('title', 'kolab')->first(); $user->assignPackage($package); $wallet = $user->wallets()->first(); $controller = new WalletsController(); $method = new \ReflectionMethod($controller, 'getWalletNotice'); $method->setAccessible(true); // User/entitlements created today, balance=0 $notice = $method->invoke($controller, $wallet); $this->assertSame('You are in your free trial period.', $notice); $wallet->owner->created_at = Carbon::now()->subDays(15); $wallet->owner->save(); $notice = $method->invoke($controller, $wallet); $this->assertSame('Your free trial is about to end, top up to continue.', $notice); // User/entitlements created today, balance=-10 CHF $wallet->balance = -1000; $notice = $method->invoke($controller, $wallet); $this->assertSame('You are out of credit, top up your balance now.', $notice); // User/entitlements created slightly more than a month ago, balance=9,99 CHF (monthly) $wallet->owner->created_at = Carbon::now()->subMonthsWithoutOverflow(1)->subDays(1); $wallet->owner->save(); // test "1 month" $wallet->balance = 990; $notice = $method->invoke($controller, $wallet); - $this->assertRegExp('/\((1 month|4 weeks)\)/', $notice); + $this->assertMatchesRegularExpression('/\((1 month|4 weeks)\)/', $notice); // test "2 months" $wallet->balance = 990 * 2.6; $notice = $method->invoke($controller, $wallet); - $this->assertRegExp('/\(2 months 2 weeks\)/', $notice); + $this->assertMatchesRegularExpression('/\(2 months 2 weeks\)/', $notice); // Change locale to make sure the text is localized by Carbon \app()->setLocale('de'); // test "almost 2 years" $wallet->balance = 990 * 23.5; $notice = $method->invoke($controller, $wallet); - $this->assertRegExp('/\(1 Jahr 11 Monate\)/', $notice); + $this->assertMatchesRegularExpression('/\(1 Jahr 11 Monate\)/', $notice); // Old entitlements, 100% discount $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subDays(40)); $discount = \App\Discount::where('discount', 100)->first(); $wallet->discount()->associate($discount); $notice = $method->invoke($controller, $wallet->refresh()); $this->assertSame(null, $notice); } /** * Test fetching pdf receipt */ public function testReceiptDownload(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); $john = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Unauth access not allowed $response = $this->get("api/v4/wallets/{$wallet->id}/receipts/2020-05"); $response->assertStatus(401); $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/receipts/2020-05"); $response->assertStatus(403); // Invalid receipt id (current month) $receiptId = date('Y-m'); $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); $response->assertStatus(404); // Invalid receipt id $receiptId = '1000-03'; $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); $response->assertStatus(404); // Valid receipt id $year = intval(date('Y')) - 1; $receiptId = "$year-12"; $filename = \config('app.name') . " Receipt for $year-12"; $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}"); $response->assertStatus(200); $response->assertHeader('content-type', 'application/pdf'); $response->assertHeader('content-disposition', 'attachment; filename="' . $filename . '"'); $response->assertHeader('content-length'); $length = $response->headers->get('content-length'); $content = $response->content(); $this->assertStringStartsWith("%PDF-1.", $content); $this->assertEquals(strlen($content), $length); } /** * Test fetching list of receipts */ public function testReceipts(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); $john = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->payments()->delete(); // Unauth access not allowed $response = $this->get("api/v4/wallets/{$wallet->id}/receipts"); $response->assertStatus(401); $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/receipts"); $response->assertStatus(403); // Empty list expected $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame([], $json['list']); $this->assertSame(1, $json['page']); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); // Insert a payment to the database $date = Carbon::create(intval(date('Y')) - 1, 4, 30); $payment = Payment::create([ 'id' => 'AAA1', 'status' => PaymentProvider::STATUS_PAID, 'type' => PaymentProvider::TYPE_ONEOFF, 'description' => 'Paid in April', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 1111, 'currency' => 'CHF', 'currency_amount' => 1111, ]); $payment->updated_at = $date; $payment->save(); $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame([$date->format('Y-m')], $json['list']); $this->assertSame(1, $json['page']); $this->assertSame(1, $json['count']); $this->assertSame(false, $json['hasMore']); } /** * Test fetching a wallet (GET /api/v4/wallets/:id) */ public function testShow(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $wallet = $john->wallets()->first(); $wallet->balance = -100; $wallet->save(); // Accessing a wallet of someone else $response = $this->actingAs($jack)->get("api/v4/wallets/{$wallet->id}"); $response->assertStatus(403); // Accessing non-existing wallet $response = $this->actingAs($jack)->get("api/v4/wallets/aaa"); $response->assertStatus(404); // Wallet owner $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($wallet->id, $json['id']); $this->assertSame('CHF', $json['currency']); $this->assertSame($wallet->balance, $json['balance']); $this->assertTrue(empty($json['description'])); $this->assertTrue(!empty($json['notice'])); } /** * Test fetching wallet transactions */ public function testTransactions(): void { $package_kolab = \App\Package::where('title', 'kolab')->first(); $user = $this->getTestUser('wallets-controller@kolabnow.com'); $user->assignPackage($package_kolab); $john = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Unauth access not allowed $response = $this->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(401); $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(403); // Expect empty list $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame([], $json['list']); $this->assertSame(1, $json['page']); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); // Create some sample transactions $transactions = $this->createTestTransactions($wallet); $transactions = array_reverse($transactions); $pages = array_chunk($transactions, 10 /* page size*/); // Get the first page $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(1, $json['page']); $this->assertSame(10, $json['count']); $this->assertSame(true, $json['hasMore']); $this->assertCount(10, $json['list']); foreach ($pages[0] as $idx => $transaction) { $this->assertSame($transaction->id, $json['list'][$idx]['id']); $this->assertSame($transaction->type, $json['list'][$idx]['type']); $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); $this->assertFalse($json['list'][$idx]['hasDetails']); $this->assertFalse(array_key_exists('user', $json['list'][$idx])); } $search = null; // Get the second page $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?page=2"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(2, $json['page']); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertCount(2, $json['list']); foreach ($pages[1] as $idx => $transaction) { $this->assertSame($transaction->id, $json['list'][$idx]['id']); $this->assertSame($transaction->type, $json['list'][$idx]['type']); $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); $this->assertSame( $transaction->type == Transaction::WALLET_DEBIT, $json['list'][$idx]['hasDetails'] ); $this->assertFalse(array_key_exists('user', $json['list'][$idx])); if ($transaction->type == Transaction::WALLET_DEBIT) { $search = $transaction->id; } } // Get a non-existing page $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?page=3"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(3, $json['page']); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertCount(0, $json['list']); // Sub-transaction searching $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?transaction=123"); $response->assertStatus(404); $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?transaction={$search}"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSame('success', $json['status']); $this->assertSame(1, $json['page']); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertCount(2, $json['list']); $this->assertSame(Transaction::ENTITLEMENT_BILLED, $json['list'][0]['type']); $this->assertSame(Transaction::ENTITLEMENT_BILLED, $json['list'][1]['type']); // Test that John gets 404 if he tries to access // someone else's transaction ID on his wallet's endpoint $wallet = $john->wallets()->first(); $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/transactions?transaction={$search}"); $response->assertStatus(404); } } diff --git a/src/tests/Feature/GroupTest.php b/src/tests/Feature/GroupTest.php index fc423b03..dedf830e 100644 --- a/src/tests/Feature/GroupTest.php +++ b/src/tests/Feature/GroupTest.php @@ -1,266 +1,266 @@ deleteTestUser('user-test@kolabnow.com'); $this->deleteTestGroup('group-test@kolabnow.com'); } public function tearDown(): void { $this->deleteTestUser('user-test@kolabnow.com'); $this->deleteTestGroup('group-test@kolabnow.com'); parent::tearDown(); } /** * Tests for Group::assignToWallet() */ public function testAssignToWallet(): void { $user = $this->getTestUser('user-test@kolabnow.com'); $group = $this->getTestGroup('group-test@kolabnow.com'); $result = $group->assignToWallet($user->wallets->first()); $this->assertSame($group, $result); $this->assertSame(1, $group->entitlement()->count()); // Can't be done twice on the same group $this->expectException(\Exception::class); $result->assignToWallet($user->wallets->first()); } /** * Test group status assignment and is*() methods */ public function testStatus(): void { $group = new Group(); $this->assertSame(false, $group->isNew()); $this->assertSame(false, $group->isActive()); $this->assertSame(false, $group->isDeleted()); $this->assertSame(false, $group->isLdapReady()); $this->assertSame(false, $group->isSuspended()); $group->status = Group::STATUS_NEW; $this->assertSame(true, $group->isNew()); $this->assertSame(false, $group->isActive()); $this->assertSame(false, $group->isDeleted()); $this->assertSame(false, $group->isLdapReady()); $this->assertSame(false, $group->isSuspended()); $group->status |= Group::STATUS_ACTIVE; $this->assertSame(true, $group->isNew()); $this->assertSame(true, $group->isActive()); $this->assertSame(false, $group->isDeleted()); $this->assertSame(false, $group->isLdapReady()); $this->assertSame(false, $group->isSuspended()); $group->status |= Group::STATUS_LDAP_READY; $this->assertSame(true, $group->isNew()); $this->assertSame(true, $group->isActive()); $this->assertSame(false, $group->isDeleted()); $this->assertSame(true, $group->isLdapReady()); $this->assertSame(false, $group->isSuspended()); $group->status |= Group::STATUS_DELETED; $this->assertSame(true, $group->isNew()); $this->assertSame(true, $group->isActive()); $this->assertSame(true, $group->isDeleted()); $this->assertSame(true, $group->isLdapReady()); $this->assertSame(false, $group->isSuspended()); $group->status |= Group::STATUS_SUSPENDED; $this->assertSame(true, $group->isNew()); $this->assertSame(true, $group->isActive()); $this->assertSame(true, $group->isDeleted()); $this->assertSame(true, $group->isLdapReady()); $this->assertSame(true, $group->isSuspended()); // Unknown status value $this->expectException(\Exception::class); $group->status = 111; } /** * Test creating a group */ public function testCreate(): void { Queue::fake(); $group = Group::create(['email' => 'GROUP-test@kolabnow.com']); $this->assertSame('group-test@kolabnow.com', $group->email); - $this->assertRegExp('/^[0-9]{1,20}$/', $group->id); + $this->assertMatchesRegularExpression('/^[0-9]{1,20}$/', $group->id); $this->assertSame([], $group->members); $this->assertTrue($group->isNew()); $this->assertTrue($group->isActive()); Queue::assertPushed( \App\Jobs\Group\CreateJob::class, function ($job) use ($group) { $groupEmail = TestCase::getObjectProperty($job, 'groupEmail'); $groupId = TestCase::getObjectProperty($job, 'groupId'); return $groupEmail === $group->email && $groupId === $group->id; } ); } /** * Test group deletion and force-deletion */ public function testDelete(): void { Queue::fake(); $user = $this->getTestUser('user-test@kolabnow.com'); $group = $this->getTestGroup('group-test@kolabnow.com'); $group->assignToWallet($user->wallets->first()); $entitlements = \App\Entitlement::where('entitleable_id', $group->id); $this->assertSame(1, $entitlements->count()); $group->delete(); $this->assertTrue($group->fresh()->trashed()); $this->assertSame(0, $entitlements->count()); $this->assertSame(1, $entitlements->withTrashed()->count()); $group->forceDelete(); $this->assertSame(0, $entitlements->withTrashed()->count()); $this->assertCount(0, Group::withTrashed()->where('id', $group->id)->get()); Queue::assertPushed(\App\Jobs\Group\DeleteJob::class, 1); Queue::assertPushed( \App\Jobs\Group\DeleteJob::class, function ($job) use ($group) { $groupEmail = TestCase::getObjectProperty($job, 'groupEmail'); $groupId = TestCase::getObjectProperty($job, 'groupId'); return $groupEmail === $group->email && $groupId === $group->id; } ); } /** * Tests for Group::emailExists() */ public function testEmailExists(): void { Queue::fake(); $group = $this->getTestGroup('group-test@kolabnow.com'); $this->assertFalse(Group::emailExists('unknown@domain.tld')); $this->assertTrue(Group::emailExists($group->email)); $result = Group::emailExists($group->email, true); $this->assertSame($result->id, $group->id); $group->delete(); $this->assertTrue(Group::emailExists($group->email)); $result = Group::emailExists($group->email, true); $this->assertSame($result->id, $group->id); } /** * Tests for Group::suspend() */ public function testSuspend(): void { Queue::fake(); $group = $this->getTestGroup('group-test@kolabnow.com'); $group->suspend(); $this->assertTrue($group->isSuspended()); Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1); Queue::assertPushed( \App\Jobs\Group\UpdateJob::class, function ($job) use ($group) { $groupEmail = TestCase::getObjectProperty($job, 'groupEmail'); $groupId = TestCase::getObjectProperty($job, 'groupId'); return $groupEmail === $group->email && $groupId === $group->id; } ); } /** * Test updating a group */ public function testUpdate(): void { Queue::fake(); $group = $this->getTestGroup('group-test@kolabnow.com'); $group->status |= Group::STATUS_DELETED; $group->save(); Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1); Queue::assertPushed( \App\Jobs\Group\UpdateJob::class, function ($job) use ($group) { $groupEmail = TestCase::getObjectProperty($job, 'groupEmail'); $groupId = TestCase::getObjectProperty($job, 'groupId'); return $groupEmail === $group->email && $groupId === $group->id; } ); } /** * Tests for Group::unsuspend() */ public function testUnsuspend(): void { Queue::fake(); $group = $this->getTestGroup('group-test@kolabnow.com'); $group->status = Group::STATUS_SUSPENDED; $group->unsuspend(); $this->assertFalse($group->isSuspended()); Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1); Queue::assertPushed( \App\Jobs\Group\UpdateJob::class, function ($job) use ($group) { $groupEmail = TestCase::getObjectProperty($job, 'groupEmail'); $groupId = TestCase::getObjectProperty($job, 'groupId'); return $groupEmail === $group->email && $groupId === $group->id; } ); } } diff --git a/src/tests/Feature/Jobs/WalletCheckTest.php b/src/tests/Feature/Jobs/WalletCheckTest.php index b627431c..fde45f2d 100644 --- a/src/tests/Feature/Jobs/WalletCheckTest.php +++ b/src/tests/Feature/Jobs/WalletCheckTest.php @@ -1,328 +1,328 @@ getTestUser('ned@kolab.org'); if ($ned->isSuspended()) { $ned->status -= User::STATUS_SUSPENDED; $ned->save(); } $this->deleteTestUser('wallet-check@kolabnow.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $ned = $this->getTestUser('ned@kolab.org'); if ($ned->isSuspended()) { $ned->status -= User::STATUS_SUSPENDED; $ned->save(); } $this->deleteTestUser('wallet-check@kolabnow.com'); parent::tearDown(); } /** * Test job handle, initial negative-balance notification */ public function testHandleInitial(): void { Mail::fake(); $user = $this->getTestUser('ned@kolab.org'); $user->setSetting('external_email', 'external@test.com'); $wallet = $user->wallets()->first(); $now = Carbon::now(); // Balance is not negative, double-update+save for proper resetting of the state $wallet->balance = -100; $wallet->save(); $wallet->balance = 0; $wallet->save(); $job = new WalletCheck($wallet); $job->handle(); Mail::assertNothingSent(); // Balance is negative now $wallet->balance = -100; $wallet->save(); $job = new WalletCheck($wallet); $job->handle(); Mail::assertNothingSent(); // Balance turned negative 2 hours ago, expect mail sent $wallet->setSetting('balance_negative_since', $now->subHours(2)->toDateTimeString()); $wallet->setSetting('balance_warning_initial', null); $job = new WalletCheck($wallet); $job->handle(); // Assert the mail was sent to the user's email, but not to his external email Mail::assertSent(\App\Mail\NegativeBalance::class, 1); Mail::assertSent(\App\Mail\NegativeBalance::class, function ($mail) use ($user) { return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com'); }); // Run the job again to make sure the notification is not sent again Mail::fake(); $job = new WalletCheck($wallet); $job->handle(); Mail::assertNothingSent(); // Test the migration scenario where a negative wallet has no balance_negative_since set yet Mail::fake(); $wallet->setSetting('balance_negative_since', null); $wallet->setSetting('balance_warning_initial', null); $job = new WalletCheck($wallet); $job->handle(); // Assert the mail was sent to the user's email, but not to his external email Mail::assertSent(\App\Mail\NegativeBalance::class, 1); Mail::assertSent(\App\Mail\NegativeBalance::class, function ($mail) use ($user) { return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com'); }); $wallet->refresh(); $today_regexp = '/' . Carbon::now()->toDateString() . ' [0-9]{2}:[0-9]{2}:[0-9]{2}/'; - $this->assertRegExp($today_regexp, $wallet->getSetting('balance_negative_since')); - $this->assertRegExp($today_regexp, $wallet->getSetting('balance_warning_initial')); + $this->assertMatchesRegularExpression($today_regexp, $wallet->getSetting('balance_negative_since')); + $this->assertMatchesRegularExpression($today_regexp, $wallet->getSetting('balance_warning_initial')); } /** * Test job handle, top-up before reminder notification * * @depends testHandleInitial */ public function testHandleBeforeReminder(): void { Mail::fake(); $user = $this->getTestUser('ned@kolab.org'); $wallet = $user->wallets()->first(); $now = Carbon::now(); // Balance turned negative 7-1 days ago $wallet->setSetting('balance_negative_since', $now->subDays(7 - 1)->toDateTimeString()); $job = new WalletCheck($wallet); $res = $job->handle(); Mail::assertNothingSent(); // TODO: Test that it actually executed the topUpWallet() $this->assertSame(WalletCheck::THRESHOLD_BEFORE_REMINDER, $res); $this->assertFalse($user->fresh()->isSuspended()); } /** * Test job handle, reminder notification * * @depends testHandleBeforeReminder */ public function testHandleReminder(): void { Mail::fake(); $user = $this->getTestUser('ned@kolab.org'); $user->setSetting('external_email', 'external@test.com'); $wallet = $user->wallets()->first(); $now = Carbon::now(); // Balance turned negative 7+1 days ago, expect mail sent $wallet->setSetting('balance_negative_since', $now->subDays(7 + 1)->toDateTimeString()); $job = new WalletCheck($wallet); $job->handle(); // Assert the mail was sent to the user's email, but not to his external email Mail::assertSent(\App\Mail\NegativeBalanceReminder::class, 1); Mail::assertSent(\App\Mail\NegativeBalanceReminder::class, function ($mail) use ($user) { return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com'); }); // Run the job again to make sure the notification is not sent again Mail::fake(); $job = new WalletCheck($wallet); $job->handle(); Mail::assertNothingSent(); } /** * Test job handle, top-up wallet before account suspending * * @depends testHandleReminder */ public function testHandleBeforeSuspended(): void { Mail::fake(); $user = $this->getTestUser('ned@kolab.org'); $wallet = $user->wallets()->first(); $now = Carbon::now(); // Balance turned negative 7+14-1 days ago $days = 7 + 14 - 1; $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString()); $job = new WalletCheck($wallet); $res = $job->handle(); Mail::assertNothingSent(); // TODO: Test that it actually executed the topUpWallet() $this->assertSame(WalletCheck::THRESHOLD_BEFORE_SUSPEND, $res); $this->assertFalse($user->fresh()->isSuspended()); } /** * Test job handle, account suspending * * @depends testHandleBeforeSuspended */ public function testHandleSuspended(): void { Mail::fake(); $user = $this->getTestUser('ned@kolab.org'); $user->setSetting('external_email', 'external@test.com'); $wallet = $user->wallets()->first(); $now = Carbon::now(); // Balance turned negative 7+14+1 days ago, expect mail sent $days = 7 + 14 + 1; $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString()); $job = new WalletCheck($wallet); $job->handle(); // Assert the mail was sent to the user's email, but not to his external email Mail::assertSent(\App\Mail\NegativeBalanceSuspended::class, 1); Mail::assertSent(\App\Mail\NegativeBalanceSuspended::class, function ($mail) use ($user) { return $mail->hasTo($user->email) && $mail->hasCc('external@test.com'); }); // Check that it has been suspended $this->assertTrue($user->fresh()->isSuspended()); // TODO: Test that group account members/domain are also being suspended /* foreach ($wallet->entitlements()->fresh()->get() as $entitlement) { if ( $entitlement->entitleable_type == \App\Domain::class || $entitlement->entitleable_type == \App\User::class ) { $this->assertTrue($entitlement->entitleable->isSuspended()); } } */ // Run the job again to make sure the notification is not sent again Mail::fake(); $job = new WalletCheck($wallet); $job->handle(); Mail::assertNothingSent(); } /** * Test job handle, final warning before delete * * @depends testHandleSuspended */ public function testHandleBeforeDelete(): void { Mail::fake(); $user = $this->getTestUser('ned@kolab.org'); $user->setSetting('external_email', 'external@test.com'); $wallet = $user->wallets()->first(); $now = Carbon::now(); // Balance turned negative 7+14+21-3+1 days ago, expect mail sent $days = 7 + 14 + 21 - 3 + 1; $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString()); $job = new WalletCheck($wallet); $job->handle(); // Assert the mail was sent to the user's email, and his external email Mail::assertSent(\App\Mail\NegativeBalanceBeforeDelete::class, 1); Mail::assertSent(\App\Mail\NegativeBalanceBeforeDelete::class, function ($mail) use ($user) { return $mail->hasTo($user->email) && $mail->hasCc('external@test.com'); }); // Check that it has not been deleted yet $this->assertFalse($user->fresh()->isDeleted()); // Run the job again to make sure the notification is not sent again Mail::fake(); $job = new WalletCheck($wallet); $job->handle(); Mail::assertNothingSent(); } /** * Test job handle, account delete * * @depends testHandleBeforeDelete */ public function testHandleDelete(): void { Mail::fake(); $user = $this->getTestUser('wallet-check@kolabnow.com'); $wallet = $user->wallets()->first(); $wallet->balance = -100; $wallet->save(); $now = Carbon::now(); $package = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $user->assignPackage($package); $this->assertFalse($user->isDeleted()); $this->assertCount(7, $user->entitlements()->get()); // Balance turned negative 7+14+21+1 days ago, expect mail sent $days = 7 + 14 + 21 + 1; $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString()); $job = new WalletCheck($wallet); $job->handle(); Mail::assertNothingSent(); // Check that it has not been deleted $this->assertTrue($user->fresh()->trashed()); $this->assertCount(0, $user->entitlements()->get()); // TODO: Test it deletes all members of the group account } } diff --git a/src/tests/Unit/DomainTest.php b/src/tests/Unit/DomainTest.php index 37ab96e4..6e3a6ce3 100644 --- a/src/tests/Unit/DomainTest.php +++ b/src/tests/Unit/DomainTest.php @@ -1,138 +1,138 @@ 'test.com', 'status' => \array_sum($domainStatuses), 'type' => Domain::TYPE_EXTERNAL ] ); $domainStatuses = []; foreach ($statuses as $status) { if ($domain->status & $status) { $domainStatuses[] = $status; } } $this->assertSame($domain->status, \array_sum($domainStatuses)); // either one is true, but not both $this->assertSame( $domain->isNew() === in_array(Domain::STATUS_NEW, $domainStatuses), $domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domainStatuses) ); $this->assertTrue( $domain->isNew() === in_array(Domain::STATUS_NEW, $domainStatuses) ); $this->assertTrue( $domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domainStatuses) ); $this->assertTrue( $domain->isConfirmed() === in_array(Domain::STATUS_CONFIRMED, $domainStatuses) ); $this->assertTrue( $domain->isSuspended() === in_array(Domain::STATUS_SUSPENDED, $domainStatuses) ); $this->assertTrue( $domain->isDeleted() === in_array(Domain::STATUS_DELETED, $domainStatuses) ); $this->assertTrue( $domain->isLdapReady() === in_array(Domain::STATUS_LDAP_READY, $domainStatuses) ); $this->assertTrue( $domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domainStatuses) ); } } /** * Test basic Domain funtionality */ public function testDomainType(): void { $types = [ Domain::TYPE_PUBLIC, Domain::TYPE_HOSTED, Domain::TYPE_EXTERNAL, ]; $domains = \App\Utils::powerSet($types); foreach ($domains as $domain_types) { $domain = new Domain( [ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, 'type' => \array_sum($domain_types), ] ); $this->assertTrue($domain->isPublic() === in_array(Domain::TYPE_PUBLIC, $domain_types)); $this->assertTrue($domain->isHosted() === in_array(Domain::TYPE_HOSTED, $domain_types)); $this->assertTrue($domain->isExternal() === in_array(Domain::TYPE_EXTERNAL, $domain_types)); } } /** * Test domain hash generation */ public function testHash(): void { $domain = new Domain([ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, ]); $hash_code = $domain->hash(); - $this->assertRegExp('/^[a-f0-9]{32}$/', $hash_code); + $this->assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $hash_code); $hash_text = $domain->hash(Domain::HASH_TEXT); - $this->assertRegExp('/^kolab-verify=[a-f0-9]{32}$/', $hash_text); + $this->assertMatchesRegularExpression('/^kolab-verify=[a-f0-9]{32}$/', $hash_text); $this->assertSame($hash_code, str_replace('kolab-verify=', '', $hash_text)); $hash_cname = $domain->hash(Domain::HASH_CNAME); $this->assertSame('kolab-verify', $hash_cname); $hash_code2 = $domain->hash(Domain::HASH_CODE); $this->assertSame($hash_code, $hash_code2); } } diff --git a/src/tests/Unit/UserTest.php b/src/tests/Unit/UserTest.php index 6ff02868..100c9110 100644 --- a/src/tests/Unit/UserTest.php +++ b/src/tests/Unit/UserTest.php @@ -1,89 +1,89 @@ 'user@email.com']); $user->password = 'test'; $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; - $this->assertRegExp('/^\$2y\$12\$[0-9a-zA-Z\/.]{53}$/', $user->password); + $this->assertMatchesRegularExpression('/^\$2y\$12\$[0-9a-zA-Z\/.]{53}$/', $user->password); $this->assertSame($ssh512, $user->password_ldap); } /** * Test User password mutator */ public function testSetPasswordLdapAttribute(): void { $user = new User(['email' => 'user@email.com']); $user->password_ldap = 'test'; $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; - $this->assertRegExp('/^\$2y\$12\$[0-9a-zA-Z\/.]{53}$/', $user->password); + $this->assertMatchesRegularExpression('/^\$2y\$12\$[0-9a-zA-Z\/.]{53}$/', $user->password); $this->assertSame($ssh512, $user->password_ldap); } /** * Test basic User funtionality */ public function testStatus(): void { $statuses = [ User::STATUS_NEW, User::STATUS_ACTIVE, User::STATUS_SUSPENDED, User::STATUS_DELETED, User::STATUS_IMAP_READY, User::STATUS_LDAP_READY, ]; $users = \App\Utils::powerSet($statuses); foreach ($users as $user_statuses) { $user = new User( [ 'email' => 'user@email.com', 'status' => \array_sum($user_statuses), ] ); $this->assertTrue($user->isNew() === in_array(User::STATUS_NEW, $user_statuses)); $this->assertTrue($user->isActive() === in_array(User::STATUS_ACTIVE, $user_statuses)); $this->assertTrue($user->isSuspended() === in_array(User::STATUS_SUSPENDED, $user_statuses)); $this->assertTrue($user->isDeleted() === in_array(User::STATUS_DELETED, $user_statuses)); $this->assertTrue($user->isLdapReady() === in_array(User::STATUS_LDAP_READY, $user_statuses)); $this->assertTrue($user->isImapReady() === in_array(User::STATUS_IMAP_READY, $user_statuses)); } } /** * Test setStatusAttribute exception */ public function testStatusInvalid(): void { $this->expectException(\Exception::class); $user = new User( [ 'email' => 'user@email.com', 'status' => 1234567, ] ); } }