diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php index eddd4d39..1a6580e1 100644 --- a/src/tests/Feature/UserTest.php +++ b/src/tests/Feature/UserTest.php @@ -1,1599 +1,1595 @@ deleteTestUser('user-test@' . \config('app.domain')); $this->deleteTestUser('UserAccountA@UserAccount.com'); $this->deleteTestUser('UserAccountB@UserAccount.com'); $this->deleteTestUser('UserAccountC@UserAccount.com'); $this->deleteTestGroup('test-group@UserAccount.com'); $this->deleteTestResource('test-resource@UserAccount.com'); $this->deleteTestSharedFolder('test-folder@UserAccount.com'); $this->deleteTestDomain('UserAccount.com'); $this->deleteTestDomain('UserAccountAdd.com'); Package::where('title', 'test-package')->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { \App\TenantSetting::truncate(); Package::where('title', 'test-package')->delete(); $this->deleteTestUser('user-test@' . \config('app.domain')); $this->deleteTestUser('UserAccountA@UserAccount.com'); $this->deleteTestUser('UserAccountB@UserAccount.com'); $this->deleteTestUser('UserAccountC@UserAccount.com'); $this->deleteTestGroup('test-group@UserAccount.com'); $this->deleteTestResource('test-resource@UserAccount.com'); $this->deleteTestSharedFolder('test-folder@UserAccount.com'); $this->deleteTestDomain('UserAccount.com'); $this->deleteTestDomain('UserAccountAdd.com'); parent::tearDown(); } /** * Tests for User::assignPackage() */ public function testAssignPackage(): void { $user = $this->getTestUser('user-test@' . \config('app.domain')); $wallet = $user->wallets()->first(); $skuGroupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first(); // cost: 490 $skuMailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); // cost: 500 $skuStorage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); // cost: 25 $package = Package::create([ 'title' => 'test-package', 'name' => 'Test Account', 'description' => 'Test account.', 'discount_rate' => 0, ]); // WARNING: saveMany() sets package_skus.cost = skus.cost $package->skus()->saveMany([ $skuMailbox, $skuGroupware, $skuStorage ]); $package->skus()->updateExistingPivot($skuStorage, ['qty' => 2, 'cost' => null], false); $package->skus()->updateExistingPivot($skuMailbox, ['cost' => null], false); $package->skus()->updateExistingPivot($skuGroupware, ['cost' => 100], false); $user->assignPackage($package); $this->assertCount(4, $user->entitlements()->get()); // mailbox + groupware + 2 x storage $entitlement = $wallet->entitlements()->where('sku_id', $skuMailbox->id)->first(); $this->assertSame($skuMailbox->id, $entitlement->sku->id); $this->assertSame($wallet->id, $entitlement->wallet->id); $this->assertEquals($user->id, $entitlement->entitleable_id); $this->assertTrue($entitlement->entitleable instanceof \App\User); $this->assertSame($skuMailbox->cost, $entitlement->cost); $entitlement = $wallet->entitlements()->where('sku_id', $skuGroupware->id)->first(); $this->assertSame($skuGroupware->id, $entitlement->sku->id); $this->assertSame($wallet->id, $entitlement->wallet->id); $this->assertEquals($user->id, $entitlement->entitleable_id); $this->assertTrue($entitlement->entitleable instanceof \App\User); $this->assertSame(100, $entitlement->cost); $entitlement = $wallet->entitlements()->where('sku_id', $skuStorage->id)->first(); $this->assertSame($skuStorage->id, $entitlement->sku->id); $this->assertSame($wallet->id, $entitlement->wallet->id); $this->assertEquals($user->id, $entitlement->entitleable_id); $this->assertTrue($entitlement->entitleable instanceof \App\User); $this->assertSame(0, $entitlement->cost); } /** * Tests for User::assignPlan() */ public function testAssignPlan(): void { $this->markTestIncomplete(); } /** * Tests for User::assignSku() */ public function testAssignSku(): void { $user = $this->getTestUser('user-test@' . \config('app.domain')); $wallet = $user->wallets()->first(); $skuStorage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); $skuMailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); $user->assignSku($skuMailbox); $this->assertCount(1, $user->entitlements()->get()); $entitlement = $wallet->entitlements()->where('sku_id', $skuMailbox->id)->first(); $this->assertSame($skuMailbox->id, $entitlement->sku->id); $this->assertSame($wallet->id, $entitlement->wallet->id); $this->assertEquals($user->id, $entitlement->entitleable_id); $this->assertTrue($entitlement->entitleable instanceof \App\User); $this->assertSame($skuMailbox->cost, $entitlement->cost); // Test units_free handling for ($x = 0; $x < 5; $x++) { $user->assignSku($skuStorage); } $entitlements = $user->entitlements()->where('sku_id', $skuStorage->id) ->where('cost', 0) ->get(); $this->assertCount(5, $entitlements); $user->assignSku($skuStorage); $entitlements = $user->entitlements()->where('sku_id', $skuStorage->id) ->where('cost', $skuStorage->cost) ->get(); $this->assertCount(1, $entitlements); } /** * Verify a wallet assigned a controller is among the accounts of the assignee. */ public function testAccounts(): void { $userA = $this->getTestUser('UserAccountA@UserAccount.com'); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $this->assertTrue($userA->wallets()->count() == 1); $userA->wallets()->each( function ($wallet) use ($userB) { $wallet->addController($userB); } ); $this->assertTrue($userB->accounts()->get()[0]->id === $userA->wallets()->get()[0]->id); } /** * Test User::canDelete() method */ public function testCanDelete(): void { $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $reseller1 = $this->getTestUser('reseller@' . \config('app.domain')); $admin = $this->getTestUser('jeroen@jeroen.jeroen'); $domain = $this->getTestDomain('kolab.org'); // Admin $this->assertTrue($admin->canDelete($admin)); $this->assertFalse($admin->canDelete($john)); $this->assertFalse($admin->canDelete($jack)); $this->assertFalse($admin->canDelete($reseller1)); $this->assertFalse($admin->canDelete($domain)); $this->assertFalse($admin->canDelete($domain->wallet())); // Reseller - kolabnow $this->assertFalse($reseller1->canDelete($john)); $this->assertFalse($reseller1->canDelete($jack)); $this->assertTrue($reseller1->canDelete($reseller1)); $this->assertFalse($reseller1->canDelete($domain)); $this->assertFalse($reseller1->canDelete($domain->wallet())); $this->assertFalse($reseller1->canDelete($admin)); // Normal user - account owner $this->assertTrue($john->canDelete($john)); $this->assertTrue($john->canDelete($ned)); $this->assertTrue($john->canDelete($jack)); $this->assertTrue($john->canDelete($domain)); $this->assertFalse($john->canDelete($domain->wallet())); $this->assertFalse($john->canDelete($reseller1)); $this->assertFalse($john->canDelete($admin)); // Normal user - a non-owner and non-controller $this->assertFalse($jack->canDelete($jack)); $this->assertFalse($jack->canDelete($john)); $this->assertFalse($jack->canDelete($domain)); $this->assertFalse($jack->canDelete($domain->wallet())); $this->assertFalse($jack->canDelete($reseller1)); $this->assertFalse($jack->canDelete($admin)); // Normal user - John's wallet controller $this->assertTrue($ned->canDelete($ned)); $this->assertTrue($ned->canDelete($john)); $this->assertTrue($ned->canDelete($jack)); $this->assertTrue($ned->canDelete($domain)); $this->assertFalse($ned->canDelete($domain->wallet())); $this->assertFalse($ned->canDelete($reseller1)); $this->assertFalse($ned->canDelete($admin)); } /** * Test User::canRead() method */ public function testCanRead(): void { $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $reseller1 = $this->getTestUser('reseller@' . \config('app.domain')); $reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local'); $admin = $this->getTestUser('jeroen@jeroen.jeroen'); $domain = $this->getTestDomain('kolab.org'); // Admin $this->assertTrue($admin->canRead($admin)); $this->assertTrue($admin->canRead($john)); $this->assertTrue($admin->canRead($jack)); $this->assertTrue($admin->canRead($reseller1)); $this->assertTrue($admin->canRead($reseller2)); $this->assertTrue($admin->canRead($domain)); $this->assertTrue($admin->canRead($domain->wallet())); // Reseller - kolabnow $this->assertTrue($reseller1->canRead($john)); $this->assertTrue($reseller1->canRead($jack)); $this->assertTrue($reseller1->canRead($reseller1)); $this->assertTrue($reseller1->canRead($domain)); $this->assertTrue($reseller1->canRead($domain->wallet())); $this->assertFalse($reseller1->canRead($reseller2)); $this->assertFalse($reseller1->canRead($admin)); // Reseller - different tenant $this->assertTrue($reseller2->canRead($reseller2)); $this->assertFalse($reseller2->canRead($john)); $this->assertFalse($reseller2->canRead($jack)); $this->assertFalse($reseller2->canRead($reseller1)); $this->assertFalse($reseller2->canRead($domain)); $this->assertFalse($reseller2->canRead($domain->wallet())); $this->assertFalse($reseller2->canRead($admin)); // Normal user - account owner $this->assertTrue($john->canRead($john)); $this->assertTrue($john->canRead($ned)); $this->assertTrue($john->canRead($jack)); $this->assertTrue($john->canRead($domain)); $this->assertTrue($john->canRead($domain->wallet())); $this->assertFalse($john->canRead($reseller1)); $this->assertFalse($john->canRead($reseller2)); $this->assertFalse($john->canRead($admin)); // Normal user - a non-owner and non-controller $this->assertTrue($jack->canRead($jack)); $this->assertFalse($jack->canRead($john)); $this->assertFalse($jack->canRead($domain)); $this->assertFalse($jack->canRead($domain->wallet())); $this->assertFalse($jack->canRead($reseller1)); $this->assertFalse($jack->canRead($reseller2)); $this->assertFalse($jack->canRead($admin)); // Normal user - John's wallet controller $this->assertTrue($ned->canRead($ned)); $this->assertTrue($ned->canRead($john)); $this->assertTrue($ned->canRead($jack)); $this->assertTrue($ned->canRead($domain)); $this->assertTrue($ned->canRead($domain->wallet())); $this->assertFalse($ned->canRead($reseller1)); $this->assertFalse($ned->canRead($reseller2)); $this->assertFalse($ned->canRead($admin)); } /** * Test User::canUpdate() method */ public function testCanUpdate(): void { $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $reseller1 = $this->getTestUser('reseller@' . \config('app.domain')); $reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local'); $admin = $this->getTestUser('jeroen@jeroen.jeroen'); $domain = $this->getTestDomain('kolab.org'); // Admin $this->assertTrue($admin->canUpdate($admin)); $this->assertTrue($admin->canUpdate($john)); $this->assertTrue($admin->canUpdate($jack)); $this->assertTrue($admin->canUpdate($reseller1)); $this->assertTrue($admin->canUpdate($reseller2)); $this->assertTrue($admin->canUpdate($domain)); $this->assertTrue($admin->canUpdate($domain->wallet())); // Reseller - kolabnow $this->assertTrue($reseller1->canUpdate($john)); $this->assertTrue($reseller1->canUpdate($jack)); $this->assertTrue($reseller1->canUpdate($reseller1)); $this->assertTrue($reseller1->canUpdate($domain)); $this->assertTrue($reseller1->canUpdate($domain->wallet())); $this->assertFalse($reseller1->canUpdate($reseller2)); $this->assertFalse($reseller1->canUpdate($admin)); // Reseller - different tenant $this->assertTrue($reseller2->canUpdate($reseller2)); $this->assertFalse($reseller2->canUpdate($john)); $this->assertFalse($reseller2->canUpdate($jack)); $this->assertFalse($reseller2->canUpdate($reseller1)); $this->assertFalse($reseller2->canUpdate($domain)); $this->assertFalse($reseller2->canUpdate($domain->wallet())); $this->assertFalse($reseller2->canUpdate($admin)); // Normal user - account owner $this->assertTrue($john->canUpdate($john)); $this->assertTrue($john->canUpdate($ned)); $this->assertTrue($john->canUpdate($jack)); $this->assertTrue($john->canUpdate($domain)); $this->assertFalse($john->canUpdate($domain->wallet())); $this->assertFalse($john->canUpdate($reseller1)); $this->assertFalse($john->canUpdate($reseller2)); $this->assertFalse($john->canUpdate($admin)); // Normal user - a non-owner and non-controller $this->assertTrue($jack->canUpdate($jack)); $this->assertFalse($jack->canUpdate($john)); $this->assertFalse($jack->canUpdate($domain)); $this->assertFalse($jack->canUpdate($domain->wallet())); $this->assertFalse($jack->canUpdate($reseller1)); $this->assertFalse($jack->canUpdate($reseller2)); $this->assertFalse($jack->canUpdate($admin)); // Normal user - John's wallet controller $this->assertTrue($ned->canUpdate($ned)); $this->assertTrue($ned->canUpdate($john)); $this->assertTrue($ned->canUpdate($jack)); $this->assertTrue($ned->canUpdate($domain)); $this->assertFalse($ned->canUpdate($domain->wallet())); $this->assertFalse($ned->canUpdate($reseller1)); $this->assertFalse($ned->canUpdate($reseller2)); $this->assertFalse($ned->canUpdate($admin)); } /** * Test user created/creating/updated observers */ public function testCreateAndUpdate(): void { Queue::fake(); $domain = \config('app.domain'); \App\Tenant::find(\config('app.tenant_id'))->setSetting('pgp.enable', '0'); $user = User::create([ 'email' => 'USER-test@' . \strtoupper($domain), 'password' => 'test', ]); $result = User::where('email', "user-test@$domain")->first(); $this->assertSame("user-test@$domain", $result->email); $this->assertSame($user->id, $result->id); $this->assertSame(User::STATUS_NEW, $result->status); $this->assertSame(0, $user->passwords()->count()); Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1); Queue::assertPushed(\App\Jobs\PGP\KeyCreateJob::class, 0); Queue::assertPushed( \App\Jobs\User\CreateJob::class, function ($job) use ($user) { $userEmail = TestCase::getObjectProperty($job, 'userEmail'); $userId = TestCase::getObjectProperty($job, 'userId'); return $userEmail === $user->email && $userId === $user->id; } ); // Test invoking KeyCreateJob $this->deleteTestUser("user-test@$domain"); \App\Tenant::find(\config('app.tenant_id'))->setSetting('pgp.enable', '1'); $user = User::create(['email' => "user-test@$domain", 'password' => 'test']); Queue::assertPushed(\App\Jobs\PGP\KeyCreateJob::class, 1); Queue::assertPushed( \App\Jobs\PGP\KeyCreateJob::class, function ($job) use ($user) { $userEmail = TestCase::getObjectProperty($job, 'userEmail'); $userId = TestCase::getObjectProperty($job, 'userId'); return $userEmail === $user->email && $userId === $user->id; } ); // Update the user, test the password change $user->setSetting('password_expiration_warning', '2020-10-10 10:10:10'); $oldPassword = $user->password; $user->password = 'test123'; $user->save(); $this->assertNotEquals($oldPassword, $user->password); $this->assertSame(0, $user->passwords()->count()); $this->assertNull($user->getSetting('password_expiration_warning')); $this->assertMatchesRegularExpression( '/^' . now()->format('Y-m-d') . ' [0-9]{2}:[0-9]{2}:[0-9]{2}$/', $user->getSetting('password_update') ); Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); Queue::assertPushed( \App\Jobs\User\UpdateJob::class, function ($job) use ($user) { $userEmail = TestCase::getObjectProperty($job, 'userEmail'); $userId = TestCase::getObjectProperty($job, 'userId'); return $userEmail === $user->email && $userId === $user->id; } ); // Update the user, test the password history $user->setSetting('password_policy', 'last:3'); $oldPassword = $user->password; $user->password = 'test1234'; $user->save(); $this->assertSame(1, $user->passwords()->count()); $this->assertSame($oldPassword, $user->passwords()->first()->password); $user->password = 'test12345'; $user->save(); $oldPassword = $user->password; $user->password = 'test123456'; $user->save(); $this->assertSame(2, $user->passwords()->count()); $this->assertSame($oldPassword, $user->passwords()->latest()->first()->password); } /** * Tests for User::domains() */ public function testDomains(): void { $user = $this->getTestUser('john@kolab.org'); $domain = $this->getTestDomain('useraccount.com', [ 'status' => Domain::STATUS_NEW | Domain::STATUS_ACTIVE, 'type' => Domain::TYPE_PUBLIC, ]); $domains = $user->domains()->pluck('namespace')->all(); $this->assertContains($domain->namespace, $domains); $this->assertContains('kolab.org', $domains); // Jack is not the wallet controller, so for him the list should not // include John's domains, kolab.org specifically $user = $this->getTestUser('jack@kolab.org'); $domains = $user->domains()->pluck('namespace')->all(); $this->assertContains($domain->namespace, $domains); $this->assertNotContains('kolab.org', $domains); // Public domains of other tenants should not be returned $tenant = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->first(); $domain->tenant_id = $tenant->id; $domain->save(); $domains = $user->domains()->pluck('namespace')->all(); $this->assertNotContains($domain->namespace, $domains); } /** * Test User::getConfig() and setConfig() methods */ public function testConfigTrait(): void { $user = $this->getTestUser('UserAccountA@UserAccount.com'); $user->setSetting('greylist_enabled', null); $user->setSetting('guam_enabled', null); $user->setSetting('password_policy', null); $user->setSetting('max_password_age', null); $user->setSetting('limit_geo', null); // greylist_enabled $this->assertSame(true, $user->getConfig()['greylist_enabled']); $result = $user->setConfig(['greylist_enabled' => false, 'unknown' => false]); $this->assertSame(['unknown' => "The requested configuration parameter is not supported."], $result); $this->assertSame(false, $user->getConfig()['greylist_enabled']); $this->assertSame('false', $user->getSetting('greylist_enabled')); $result = $user->setConfig(['greylist_enabled' => true]); $this->assertSame([], $result); $this->assertSame(true, $user->getConfig()['greylist_enabled']); $this->assertSame('true', $user->getSetting('greylist_enabled')); // guam_enabled $this->assertSame(false, $user->getConfig()['guam_enabled']); $result = $user->setConfig(['guam_enabled' => false]); $this->assertSame([], $result); $this->assertSame(false, $user->getConfig()['guam_enabled']); $this->assertSame(null, $user->getSetting('guam_enabled')); $result = $user->setConfig(['guam_enabled' => true]); $this->assertSame([], $result); $this->assertSame(true, $user->getConfig()['guam_enabled']); $this->assertSame('true', $user->getSetting('guam_enabled')); // max_apssword_age $this->assertSame(null, $user->getConfig()['max_password_age']); $result = $user->setConfig(['max_password_age' => -1]); $this->assertSame([], $result); $this->assertSame(null, $user->getConfig()['max_password_age']); $this->assertSame(null, $user->getSetting('max_password_age')); $result = $user->setConfig(['max_password_age' => 12]); $this->assertSame([], $result); $this->assertSame('12', $user->getConfig()['max_password_age']); $this->assertSame('12', $user->getSetting('max_password_age')); // password_policy $result = $user->setConfig(['password_policy' => true]); $this->assertSame(['password_policy' => "Specified password policy is invalid."], $result); $this->assertSame(null, $user->getConfig()['password_policy']); $this->assertSame(null, $user->getSetting('password_policy')); $result = $user->setConfig(['password_policy' => 'min:-1']); $this->assertSame(['password_policy' => "Specified password policy is invalid."], $result); $result = $user->setConfig(['password_policy' => 'min:-1']); $this->assertSame(['password_policy' => "Specified password policy is invalid."], $result); $result = $user->setConfig(['password_policy' => 'min:10,unknown']); $this->assertSame(['password_policy' => "Specified password policy is invalid."], $result); \config(['app.password_policy' => 'min:5,max:100']); $result = $user->setConfig(['password_policy' => 'min:4,max:255']); $this->assertSame(['password_policy' => "Minimum password length cannot be less than 5."], $result); \config(['app.password_policy' => 'min:5,max:100']); $result = $user->setConfig(['password_policy' => 'min:10,max:255']); $this->assertSame(['password_policy' => "Maximum password length cannot be more than 100."], $result); \config(['app.password_policy' => 'min:5,max:255']); $result = $user->setConfig(['password_policy' => 'min:10,max:255']); $this->assertSame([], $result); $this->assertSame('min:10,max:255', $user->getConfig()['password_policy']); $this->assertSame('min:10,max:255', $user->getSetting('password_policy')); // limit_geo $this->assertSame([], $user->getConfig()['limit_geo']); $result = $user->setConfig(['limit_geo' => '']); $err = "Specified configuration is invalid. Expected a list of two-letter country codes."; $this->assertSame(['limit_geo' => $err], $result); $this->assertSame(null, $user->getSetting('limit_geo')); $result = $user->setConfig(['limit_geo' => ['usa']]); $this->assertSame(['limit_geo' => $err], $result); $this->assertSame(null, $user->getSetting('limit_geo')); $result = $user->setConfig(['limit_geo' => []]); $this->assertSame([], $result); $this->assertSame(null, $user->getSetting('limit_geo')); $result = $user->setConfig(['limit_geo' => ['US', 'ru']]); $this->assertSame([], $result); $this->assertSame(['US', 'RU'], $user->getConfig()['limit_geo']); $this->assertSame('["US","RU"]', $user->getSetting('limit_geo')); } /** * Test user account degradation and un-degradation */ public function testDegradeAndUndegrade(): void { $this->fakeQueueReset(); // Test an account with users, domain $userA = $this->getTestUser('UserAccountA@UserAccount.com'); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $domain = $this->getTestDomain('UserAccount.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_HOSTED, ]); $userA->assignPackage($package_kolab); $domain->assignPackage($package_domain, $userA); $userA->assignPackage($package_kolab, $userB); $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id); $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id); $entitlementsDomain = \App\Entitlement::where('entitleable_id', $domain->id); $yesterday = Carbon::now()->subDays(1); $this->backdateEntitlements($entitlementsA->get(), $yesterday, Carbon::now()->subMonthsWithoutOverflow(1)); $this->backdateEntitlements($entitlementsB->get(), $yesterday, Carbon::now()->subMonthsWithoutOverflow(1)); $wallet = $userA->wallets->first(); $this->assertSame(7, $entitlementsA->count()); $this->assertSame(7, $entitlementsB->count()); $this->assertSame(7, $entitlementsA->whereDate('updated_at', $yesterday->toDateString())->count()); $this->assertSame(7, $entitlementsB->whereDate('updated_at', $yesterday->toDateString())->count()); $this->assertSame(0, $wallet->balance); $this->fakeQueueReset(); // Degrade the account/wallet owner $userA->degrade(); $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id); $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id); $this->assertTrue($userA->fresh()->isDegraded()); $this->assertTrue($userA->fresh()->isDegraded(true)); $this->assertFalse($userB->fresh()->isDegraded()); $this->assertTrue($userB->fresh()->isDegraded(true)); $balance = $wallet->fresh()->balance; $this->assertTrue($balance < 0); $this->assertSame(7, $entitlementsA->whereDate('updated_at', Carbon::now()->toDateString())->count()); $this->assertSame(7, $entitlementsB->whereDate('updated_at', Carbon::now()->toDateString())->count()); // Expect one update job for every user // @phpstan-ignore-next-line $userIds = Queue::pushed(\App\Jobs\User\UpdateJob::class)->map(function ($job) { return TestCase::getObjectProperty($job, 'userId'); })->all(); $this->assertSame([$userA->id, $userB->id], $userIds); // Un-Degrade the account/wallet owner $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id); $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id); $yesterday = Carbon::now()->subDays(1); $this->backdateEntitlements($entitlementsA->get(), $yesterday, Carbon::now()->subMonthsWithoutOverflow(1)); $this->backdateEntitlements($entitlementsB->get(), $yesterday, Carbon::now()->subMonthsWithoutOverflow(1)); $this->fakeQueueReset(); $userA->undegrade(); $this->assertFalse($userA->fresh()->isDegraded()); $this->assertFalse($userA->fresh()->isDegraded(true)); $this->assertFalse($userB->fresh()->isDegraded()); $this->assertFalse($userB->fresh()->isDegraded(true)); // Expect no balance change, degraded account entitlements are free $this->assertSame($balance, $wallet->fresh()->balance); $this->assertSame(7, $entitlementsA->whereDate('updated_at', Carbon::now()->toDateString())->count()); $this->assertSame(7, $entitlementsB->whereDate('updated_at', Carbon::now()->toDateString())->count()); // Expect one update job for every user // @phpstan-ignore-next-line $userIds = Queue::pushed(\App\Jobs\User\UpdateJob::class)->map(function ($job) { return TestCase::getObjectProperty($job, 'userId'); })->all(); $this->assertSame([$userA->id, $userB->id], $userIds); } /** * Test user deletion */ public function testDelete(): void { Queue::fake(); $user = $this->getTestUser('user-test@' . \config('app.domain')); $package = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $user->assignPackage($package); $id = $user->id; $this->assertCount(7, $user->entitlements()->get()); $user->delete(); $this->assertCount(0, $user->entitlements()->get()); $this->assertTrue($user->fresh()->trashed()); $this->assertFalse($user->fresh()->isDeleted()); // Delete the user for real $job = new \App\Jobs\User\DeleteJob($id); $job->handle(); $this->assertTrue(User::withTrashed()->where('id', $id)->first()->isDeleted()); $user->forceDelete(); $this->assertCount(0, User::withTrashed()->where('id', $id)->get()); // Test an account with users, domain, and group, and resource $userA = $this->getTestUser('UserAccountA@UserAccount.com'); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $userC = $this->getTestUser('UserAccountC@UserAccount.com'); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $domain = $this->getTestDomain('UserAccount.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_HOSTED, ]); $userA->assignPackage($package_kolab); $domain->assignPackage($package_domain, $userA); $userA->assignPackage($package_kolab, $userB); $userA->assignPackage($package_kolab, $userC); $group = $this->getTestGroup('test-group@UserAccount.com'); $group->assignToWallet($userA->wallets->first()); $resource = $this->getTestResource('test-resource@UserAccount.com', ['name' => 'test']); $resource->assignToWallet($userA->wallets->first()); $folder = $this->getTestSharedFolder('test-folder@UserAccount.com', ['name' => 'test']); $folder->assignToWallet($userA->wallets->first()); $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id); $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id); $entitlementsC = \App\Entitlement::where('entitleable_id', $userC->id); $entitlementsDomain = \App\Entitlement::where('entitleable_id', $domain->id); $entitlementsGroup = \App\Entitlement::where('entitleable_id', $group->id); $entitlementsResource = \App\Entitlement::where('entitleable_id', $resource->id); $entitlementsFolder = \App\Entitlement::where('entitleable_id', $folder->id); $this->assertSame(7, $entitlementsA->count()); $this->assertSame(7, $entitlementsB->count()); $this->assertSame(7, $entitlementsC->count()); $this->assertSame(1, $entitlementsDomain->count()); $this->assertSame(1, $entitlementsGroup->count()); $this->assertSame(1, $entitlementsResource->count()); $this->assertSame(1, $entitlementsFolder->count()); // Delete non-controller user $userC->delete(); $this->assertTrue($userC->fresh()->trashed()); $this->assertFalse($userC->fresh()->isDeleted()); $this->assertSame(0, $entitlementsC->count()); // Delete the controller (and expect "sub"-users to be deleted too) $userA->delete(); $this->assertSame(0, $entitlementsA->count()); $this->assertSame(0, $entitlementsB->count()); $this->assertSame(0, $entitlementsDomain->count()); $this->assertSame(0, $entitlementsGroup->count()); $this->assertSame(0, $entitlementsResource->count()); $this->assertSame(0, $entitlementsFolder->count()); $this->assertSame(7, $entitlementsA->withTrashed()->count()); $this->assertSame(7, $entitlementsB->withTrashed()->count()); $this->assertSame(7, $entitlementsC->withTrashed()->count()); $this->assertSame(1, $entitlementsDomain->withTrashed()->count()); $this->assertSame(1, $entitlementsGroup->withTrashed()->count()); $this->assertSame(1, $entitlementsResource->withTrashed()->count()); $this->assertSame(1, $entitlementsFolder->withTrashed()->count()); $this->assertTrue($userA->fresh()->trashed()); $this->assertTrue($userB->fresh()->trashed()); $this->assertTrue($domain->fresh()->trashed()); $this->assertTrue($group->fresh()->trashed()); $this->assertTrue($resource->fresh()->trashed()); $this->assertTrue($folder->fresh()->trashed()); $this->assertFalse($userA->isDeleted()); $this->assertFalse($userB->isDeleted()); $this->assertFalse($domain->isDeleted()); $this->assertFalse($group->isDeleted()); $this->assertFalse($resource->isDeleted()); $this->assertFalse($folder->isDeleted()); $userA->forceDelete(); $all_entitlements = \App\Entitlement::where('wallet_id', $userA->wallets->first()->id); $transactions = \App\Transaction::where('object_id', $userA->wallets->first()->id); $this->assertSame(0, $all_entitlements->withTrashed()->count()); $this->assertSame(0, $transactions->count()); $this->assertCount(0, User::withTrashed()->where('id', $userA->id)->get()); $this->assertCount(0, User::withTrashed()->where('id', $userB->id)->get()); $this->assertCount(0, User::withTrashed()->where('id', $userC->id)->get()); $this->assertCount(0, Domain::withTrashed()->where('id', $domain->id)->get()); $this->assertCount(0, Group::withTrashed()->where('id', $group->id)->get()); $this->assertCount(0, \App\Resource::withTrashed()->where('id', $resource->id)->get()); $this->assertCount(0, \App\SharedFolder::withTrashed()->where('id', $folder->id)->get()); } /** * Test eventlog on user deletion */ public function testDeleteAndEventLog(): void { Queue::fake(); $user = $this->getTestUser('user-test@' . \config('app.domain')); EventLog::createFor($user, EventLog::TYPE_SUSPENDED, 'test'); $user->delete(); $this->assertCount(1, EventLog::where('object_id', $user->id)->where('object_type', User::class)->get()); $user->forceDelete(); $this->assertCount(0, EventLog::where('object_id', $user->id)->where('object_type', User::class)->get()); } /** * Test user deletion vs. group membership * * The first Queue::assertPushed is sometimes 1 and sometimes 2 * @group skipci */ public function testDeleteAndGroups(): void { Queue::fake(); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $userA = $this->getTestUser('UserAccountA@UserAccount.com'); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $userA->assignPackage($package_kolab, $userB); $group = $this->getTestGroup('test-group@UserAccount.com'); $group->members = ['test@gmail.com', $userB->email]; $group->assignToWallet($userA->wallets->first()); $group->save(); Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1); $userGroups = $userA->groups()->get(); $this->assertSame(1, $userGroups->count()); $this->assertSame($group->id, $userGroups->first()->id); $userB->delete(); $this->assertSame(['test@gmail.com'], $group->fresh()->members); // Twice, one for save() and one for delete() above Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 2); } /** * Test handling negative balance on user deletion */ public function testDeleteWithNegativeBalance(): void { $user = $this->getTestUser('user-test@' . \config('app.domain')); $wallet = $user->wallets()->first(); $wallet->balance = -1000; $wallet->save(); $reseller_wallet = $user->tenant->wallet(); $reseller_wallet->balance = 0; $reseller_wallet->save(); \App\Transaction::where('object_id', $reseller_wallet->id)->where('object_type', \App\Wallet::class)->delete(); $user->delete(); $reseller_transactions = \App\Transaction::where('object_id', $reseller_wallet->id) ->where('object_type', \App\Wallet::class)->get(); - $this->assertSame(-1000, $reseller_wallet->fresh()->balance); - $this->assertCount(1, $reseller_transactions); - $trans = $reseller_transactions[0]; - $this->assertSame("Deleted user {$user->email}", $trans->description); - $this->assertSame(-1000, $trans->amount); - $this->assertSame(\App\Transaction::WALLET_DEBIT, $trans->type); + $this->assertSame(0, $reseller_wallet->fresh()->balance); + $this->assertCount(0, $reseller_transactions); } /** * Test handling positive balance on user deletion */ public function testDeleteWithPositiveBalance(): void { $user = $this->getTestUser('user-test@' . \config('app.domain')); $wallet = $user->wallets()->first(); $wallet->balance = 1000; $wallet->save(); $reseller_wallet = $user->tenant->wallet(); $reseller_wallet->balance = 0; $reseller_wallet->save(); $user->delete(); $this->assertSame(0, $reseller_wallet->fresh()->balance); } /** * Test user deletion with PGP/WOAT enabled */ public function testDeleteWithPGP(): void { Queue::fake(); // Test with PGP disabled $user = $this->getTestUser('user-test@' . \config('app.domain')); $user->tenant->setSetting('pgp.enable', 0); $user->delete(); Queue::assertPushed(\App\Jobs\PGP\KeyDeleteJob::class, 0); // Test with PGP enabled $this->deleteTestUser('user-test@' . \config('app.domain')); $user = $this->getTestUser('user-test@' . \config('app.domain')); $user->tenant->setSetting('pgp.enable', 1); $user->delete(); $user->tenant->setSetting('pgp.enable', 0); Queue::assertPushed(\App\Jobs\PGP\KeyDeleteJob::class, 1); Queue::assertPushed( \App\Jobs\PGP\KeyDeleteJob::class, function ($job) use ($user) { $userId = TestCase::getObjectProperty($job, 'userId'); $userEmail = TestCase::getObjectProperty($job, 'userEmail'); return $userId == $user->id && $userEmail === $user->email; } ); } /** * Test user deletion vs. rooms */ public function testDeleteWithRooms(): void { $this->markTestIncomplete(); } /** * Tests for User::aliasExists() */ public function testAliasExists(): void { $this->assertTrue(User::aliasExists('jack.daniels@kolab.org')); $this->assertFalse(User::aliasExists('j.daniels@kolab.org')); $this->assertFalse(User::aliasExists('john@kolab.org')); } /** * Tests for User::emailExists() */ public function testEmailExists(): void { $this->assertFalse(User::emailExists('jack.daniels@kolab.org')); $this->assertFalse(User::emailExists('j.daniels@kolab.org')); $this->assertTrue(User::emailExists('john@kolab.org')); $user = User::emailExists('john@kolab.org', true); $this->assertSame('john@kolab.org', $user->email); } /** * Tests for User::findByEmail() */ public function testFindByEmail(): void { $user = $this->getTestUser('john@kolab.org'); $result = User::findByEmail('john'); $this->assertNull($result); $result = User::findByEmail('non-existing@email.com'); $this->assertNull($result); $result = User::findByEmail('john@kolab.org'); $this->assertInstanceOf(User::class, $result); $this->assertSame($user->id, $result->id); // Use an alias $result = User::findByEmail('john.doe@kolab.org'); $this->assertInstanceOf(User::class, $result); $this->assertSame($user->id, $result->id); Queue::fake(); // A case where two users have the same alias $ned = $this->getTestUser('ned@kolab.org'); $ned->setAliases(['joe.monster@kolab.org']); $result = User::findByEmail('joe.monster@kolab.org'); $this->assertNull($result); $ned->setAliases([]); // TODO: searching by external email (setting) $this->markTestIncomplete(); } /** * Test User::hasSku() and countEntitlementsBySku() methods */ public function testHasSku(): void { $john = $this->getTestUser('john@kolab.org'); $this->assertTrue($john->hasSku('mailbox')); $this->assertTrue($john->hasSku('storage')); $this->assertFalse($john->hasSku('beta')); $this->assertFalse($john->hasSku('unknown')); $this->assertSame(0, $john->countEntitlementsBySku('unknown')); $this->assertSame(0, $john->countEntitlementsBySku('2fa')); $this->assertSame(1, $john->countEntitlementsBySku('mailbox')); $this->assertSame(5, $john->countEntitlementsBySku('storage')); } /** * Test User::name() */ public function testName(): void { Queue::fake(); $user = $this->getTestUser('user-test@' . \config('app.domain')); $this->assertSame('', $user->name()); $this->assertSame($user->tenant->title . ' User', $user->name(true)); $user->setSetting('first_name', 'First'); $this->assertSame('First', $user->name()); $this->assertSame('First', $user->name(true)); $user->setSetting('last_name', 'Last'); $this->assertSame('First Last', $user->name()); $this->assertSame('First Last', $user->name(true)); } /** * Test resources() method */ public function testResources(): void { $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $resources = $john->resources()->orderBy('email')->get(); $this->assertSame(2, $resources->count()); $this->assertSame('resource-test1@kolab.org', $resources[0]->email); $this->assertSame('resource-test2@kolab.org', $resources[1]->email); $resources = $ned->resources()->orderBy('email')->get(); $this->assertSame(2, $resources->count()); $this->assertSame('resource-test1@kolab.org', $resources[0]->email); $this->assertSame('resource-test2@kolab.org', $resources[1]->email); $resources = $jack->resources()->get(); $this->assertSame(0, $resources->count()); } /** * Test sharedFolders() method */ public function testSharedFolders(): void { $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $folders = $john->sharedFolders()->orderBy('email')->get(); $this->assertSame(2, $folders->count()); $this->assertSame('folder-contact@kolab.org', $folders[0]->email); $this->assertSame('folder-event@kolab.org', $folders[1]->email); $folders = $ned->sharedFolders()->orderBy('email')->get(); $this->assertSame(2, $folders->count()); $this->assertSame('folder-contact@kolab.org', $folders[0]->email); $this->assertSame('folder-event@kolab.org', $folders[1]->email); $folders = $jack->sharedFolders()->get(); $this->assertSame(0, $folders->count()); } /** * Test user restoring */ public function testRestore(): void { $this->fakeQueueReset(); // Test an account with users and domain $userA = $this->getTestUser('UserAccountA@UserAccount.com', [ 'status' => User::STATUS_LDAP_READY | User::STATUS_IMAP_READY | User::STATUS_SUSPENDED, ]); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $domainA = $this->getTestDomain('UserAccount.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_HOSTED, ]); $domainB = $this->getTestDomain('UserAccountAdd.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_HOSTED, ]); $userA->assignPackage($package_kolab); $domainA->assignPackage($package_domain, $userA); $domainB->assignPackage($package_domain, $userA); $userA->assignPackage($package_kolab, $userB); $storage_sku = \App\Sku::withEnvTenantContext()->where('title', 'storage')->first(); $now = \Carbon\Carbon::now(); $wallet_id = $userA->wallets->first()->id; // add an extra storage entitlement $ent1 = \App\Entitlement::create([ 'wallet_id' => $wallet_id, 'sku_id' => $storage_sku->id, 'cost' => 0, 'entitleable_id' => $userA->id, 'entitleable_type' => User::class, ]); $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id); $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id); $entitlementsDomain = \App\Entitlement::where('entitleable_id', $domainA->id); // First delete the user $userA->delete(); $this->assertSame(0, $entitlementsA->count()); $this->assertSame(0, $entitlementsB->count()); $this->assertSame(0, $entitlementsDomain->count()); $this->assertTrue($userA->fresh()->trashed()); $this->assertTrue($userB->fresh()->trashed()); $this->assertTrue($domainA->fresh()->trashed()); $this->assertTrue($domainB->fresh()->trashed()); $this->assertFalse($userA->isDeleted()); $this->assertFalse($userB->isDeleted()); $this->assertFalse($domainA->isDeleted()); // Backdate one storage entitlement (it's not expected to be restored) \App\Entitlement::withTrashed()->where('id', $ent1->id) ->update(['deleted_at' => $now->copy()->subMinutes(2)]); // Backdate entitlements to assert that they were restored with proper updated_at timestamp \App\Entitlement::withTrashed()->where('wallet_id', $wallet_id) ->update(['updated_at' => $now->subMinutes(10)]); $this->fakeQueueReset(); // Then restore it $userA->restore(); $userA->refresh(); $this->assertFalse($userA->trashed()); $this->assertFalse($userA->isDeleted()); $this->assertFalse($userA->isSuspended()); $this->assertFalse($userA->isLdapReady()); $this->assertFalse($userA->isImapReady()); $this->assertFalse($userA->isActive()); $this->assertTrue($userA->isNew()); $this->assertTrue($userB->fresh()->trashed()); $this->assertTrue($domainB->fresh()->trashed()); $this->assertFalse($domainA->fresh()->trashed()); // Assert entitlements $this->assertSame(7, $entitlementsA->count()); // mailbox + groupware + 5 x storage $this->assertTrue($ent1->fresh()->trashed()); $entitlementsA->get()->each(function ($ent) { $this->assertTrue($ent->updated_at->greaterThan(\Carbon\Carbon::now()->subSeconds(5))); }); // We expect only CreateJob + UpdateJob pair for both user and domain. // Because how Illuminate/Database/Eloquent/SoftDeletes::restore() method // is implemented we cannot skip the UpdateJob in any way. // I don't want to overwrite this method, the extra job shouldn't do any harm. $this->assertCount(4, Queue::pushedJobs()); // @phpstan-ignore-line Queue::assertPushed(\App\Jobs\Domain\UpdateJob::class, 1); Queue::assertPushed(\App\Jobs\Domain\CreateJob::class, 1); Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1); Queue::assertPushed( \App\Jobs\User\CreateJob::class, function ($job) use ($userA) { return $userA->id === TestCase::getObjectProperty($job, 'userId'); } ); } /** * Test user account restrict() and unrestrict() */ public function testRestrictAndUnrestrict(): void { $this->fakeQueueReset(); // Test an account with users, domain $user = $this->getTestUser('UserAccountA@UserAccount.com'); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $domain = $this->getTestDomain('UserAccount.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_HOSTED, ]); $user->assignPackage($package_kolab); $domain->assignPackage($package_domain, $user); $user->assignPackage($package_kolab, $userB); $this->assertFalse($user->isRestricted()); $this->assertFalse($userB->isRestricted()); $user->restrict(); $this->assertTrue($user->fresh()->isRestricted()); $this->assertFalse($userB->fresh()->isRestricted()); Queue::assertPushed( \App\Jobs\User\UpdateJob::class, function ($job) use ($user) { return TestCase::getObjectProperty($job, 'userId') == $user->id; } ); $userB->restrict(); $this->assertTrue($userB->fresh()->isRestricted()); $this->fakeQueueReset(); $user->refresh(); $user->unrestrict(); $this->assertFalse($user->fresh()->isRestricted()); $this->assertTrue($userB->fresh()->isRestricted()); Queue::assertPushed( \App\Jobs\User\UpdateJob::class, function ($job) use ($user) { return TestCase::getObjectProperty($job, 'userId') == $user->id; } ); $this->fakeQueueReset(); $user->unrestrict(true); $this->assertFalse($user->fresh()->isRestricted()); $this->assertFalse($userB->fresh()->isRestricted()); Queue::assertPushed( \App\Jobs\User\UpdateJob::class, function ($job) use ($userB) { return TestCase::getObjectProperty($job, 'userId') == $userB->id; } ); } /** * Tests for AliasesTrait::setAliases() */ public function testSetAliases(): void { $this->fakeQueueReset(); $user = $this->getTestUser('UserAccountA@UserAccount.com'); $domain = $this->getTestDomain('UserAccount.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_HOSTED, ]); $this->assertCount(0, $user->aliases->all()); $user->tenant->setSetting('pgp.enable', 1); // Add an alias $user->setAliases(['UserAlias1@UserAccount.com']); Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); Queue::assertPushed(\App\Jobs\PGP\KeyCreateJob::class, 1); $user->tenant->setSetting('pgp.enable', 0); $aliases = $user->aliases()->get(); $this->assertCount(1, $aliases); $this->assertSame('useralias1@useraccount.com', $aliases[0]['alias']); // Add another alias $this->fakeQueueReset(); $user->setAliases(['UserAlias1@UserAccount.com', 'UserAlias2@UserAccount.com']); Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); Queue::assertPushed(\App\Jobs\PGP\KeyCreateJob::class, 0); $aliases = $user->aliases()->orderBy('alias')->get(); $this->assertCount(2, $aliases); $this->assertSame('useralias1@useraccount.com', $aliases[0]->alias); $this->assertSame('useralias2@useraccount.com', $aliases[1]->alias); $user->tenant->setSetting('pgp.enable', 1); // Remove an alias $this->fakeQueueReset(); $user->setAliases(['UserAlias1@UserAccount.com']); $user->tenant->setSetting('pgp.enable', 0); Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); Queue::assertPushed(\App\Jobs\PGP\KeyDeleteJob::class, 1); Queue::assertPushed( \App\Jobs\PGP\KeyDeleteJob::class, function ($job) use ($user) { $userId = TestCase::getObjectProperty($job, 'userId'); $userEmail = TestCase::getObjectProperty($job, 'userEmail'); return $userId == $user->id && $userEmail === 'useralias2@useraccount.com'; } ); $aliases = $user->aliases()->get(); $this->assertCount(1, $aliases); $this->assertSame('useralias1@useraccount.com', $aliases[0]['alias']); // Remove all aliases $this->fakeQueueReset(); $user->setAliases([]); Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); $this->assertCount(0, $user->aliases()->get()); } /** * Tests for suspendAccount() */ public function testSuspendAccount(): void { $user = $this->getTestUser('UserAccountA@UserAccount.com'); $wallet = $user->wallets()->first(); // No entitlements, expect the wallet owner to be suspended anyway $user->suspendAccount(); $this->assertTrue($user->fresh()->isSuspended()); // Add entitlements and more suspendable objects into the wallet $user->unsuspend(); $mailbox_sku = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); $domain_sku = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $group_sku = Sku::withEnvTenantContext()->where('title', 'group')->first(); $resource_sku = Sku::withEnvTenantContext()->where('title', 'resource')->first(); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $userB->assignSku($mailbox_sku, 1, $wallet); $domain = $this->getTestDomain('UserAccount.com', ['type' => \App\Domain::TYPE_PUBLIC]); $domain->assignSku($domain_sku, 1, $wallet); $group = $this->getTestGroup('test-group@UserAccount.com'); $group->assignSku($group_sku, 1, $wallet); $resource = $this->getTestResource('test-resource@UserAccount.com'); $resource->assignSku($resource_sku, 1, $wallet); $this->assertFalse($user->isSuspended()); $this->assertFalse($userB->isSuspended()); $this->assertFalse($domain->isSuspended()); $this->assertFalse($group->isSuspended()); $this->assertFalse($resource->isSuspended()); $user->suspendAccount(); $this->assertTrue($user->fresh()->isSuspended()); $this->assertTrue($userB->fresh()->isSuspended()); $this->assertTrue($domain->fresh()->isSuspended()); $this->assertTrue($group->fresh()->isSuspended()); $this->assertFalse($resource->fresh()->isSuspended()); } /** * Tests for UserSettingsTrait::setSettings() and getSetting() and getSettings() */ public function testUserSettings(): void { $this->fakeQueueReset(); $user = $this->getTestUser('UserAccountA@UserAccount.com'); Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 0); // Test default settings // Note: Technicly this tests UserObserver::created() behavior $all_settings = $user->settings()->orderBy('key')->get(); $this->assertCount(2, $all_settings); $this->assertSame('country', $all_settings[0]->key); $this->assertSame('CH', $all_settings[0]->value); $this->assertSame('currency', $all_settings[1]->key); $this->assertSame('CHF', $all_settings[1]->value); // Add a setting $user->setSetting('first_name', 'Firstname'); if (\config('app.with_ldap')) { Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); } // Note: We test both current user as well as fresh user object // to make sure cache works as expected $this->assertSame('Firstname', $user->getSetting('first_name')); $this->assertSame('Firstname', $user->fresh()->getSetting('first_name')); // Update a setting $this->fakeQueueReset(); $user->setSetting('first_name', 'Firstname1'); if (\config('app.with_ldap')) { Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); } // Note: We test both current user as well as fresh user object // to make sure cache works as expected $this->assertSame('Firstname1', $user->getSetting('first_name')); $this->assertSame('Firstname1', $user->fresh()->getSetting('first_name')); // Delete a setting (null) $this->fakeQueueReset(); $user->setSetting('first_name', null); if (\config('app.with_ldap')) { Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); } // Note: We test both current user as well as fresh user object // to make sure cache works as expected $this->assertSame(null, $user->getSetting('first_name')); $this->assertSame(null, $user->fresh()->getSetting('first_name')); // Delete a setting (empty string) $this->fakeQueueReset(); $user->setSetting('first_name', 'Firstname1'); $user->setSetting('first_name', ''); if (\config('app.with_ldap')) { Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); } // Note: We test both current user as well as fresh user object // to make sure cache works as expected $this->assertSame(null, $user->getSetting('first_name')); $this->assertSame(null, $user->fresh()->getSetting('first_name')); // Set multiple settings at once $this->fakeQueueReset(); $user->setSettings([ 'first_name' => 'Firstname2', 'last_name' => 'Lastname2', 'country' => null, ]); // Thanks to job locking it creates a single UserUpdate job if (\config('app.with_ldap')) { Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 1); } // Note: We test both current user as well as fresh user object // to make sure cache works as expected $this->assertSame('Firstname2', $user->getSetting('first_name')); $this->assertSame('Firstname2', $user->fresh()->getSetting('first_name')); $this->assertSame('Lastname2', $user->getSetting('last_name')); $this->assertSame('Lastname2', $user->fresh()->getSetting('last_name')); $this->assertSame(null, $user->getSetting('country')); $this->assertSame(null, $user->fresh()->getSetting('country')); $expected = [ 'currency' => 'CHF', 'first_name' => 'Firstname2', 'last_name' => 'Lastname2', ]; $this->assertSame($expected, $user->settings()->orderBy('key')->get()->pluck('value', 'key')->all()); $expected = [ 'first_name' => 'Firstname2', 'last_name' => 'Lastname2', 'unknown' => null, ]; $this->assertSame($expected, $user->getSettings(['first_name', 'last_name', 'unknown'])); } /** * Tests for User::users() */ public function testUsers(): void { $jack = $this->getTestUser('jack@kolab.org'); $joe = $this->getTestUser('joe@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $wallet = $john->wallets()->first(); $users = $john->users()->orderBy('email')->get(); $this->assertCount(4, $users); $this->assertEquals($jack->id, $users[0]->id); $this->assertEquals($joe->id, $users[1]->id); $this->assertEquals($john->id, $users[2]->id); $this->assertEquals($ned->id, $users[3]->id); $users = $jack->users()->orderBy('email')->get(); $this->assertCount(0, $users); $users = $ned->users()->orderBy('email')->get(); $this->assertCount(4, $users); } /** * Tests for User::walletOwner() (from EntitleableTrait) */ public function testWalletOwner(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $this->assertSame($john->id, $john->walletOwner()->id); $this->assertSame($john->id, $jack->walletOwner()->id); $this->assertSame($john->id, $ned->walletOwner()->id); // User with no entitlements $user = $this->getTestUser('UserAccountA@UserAccount.com'); $this->assertSame($user->id, $user->walletOwner()->id); } /** * Tests for User::wallets() */ public function testWallets(): void { $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $this->assertSame(1, $john->wallets()->count()); $this->assertCount(1, $john->wallets); $this->assertInstanceOf(\App\Wallet::class, $john->wallets->first()); $this->assertSame(1, $ned->wallets()->count()); $this->assertCount(1, $ned->wallets); $this->assertInstanceOf(\App\Wallet::class, $ned->wallets->first()); } } diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php index 46eafff0..cb1b75a8 100644 --- a/src/tests/Feature/WalletTest.php +++ b/src/tests/Feature/WalletTest.php @@ -1,1042 +1,1050 @@ users as $user) { $this->deleteTestUser($user); } Sku::select()->update(['fee' => 0]); Payment::query()->delete(); VatRate::query()->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { foreach ($this->users as $user) { $this->deleteTestUser($user); } Sku::select()->update(['fee' => 0]); Payment::query()->delete(); VatRate::query()->delete(); Plan::withEnvTenantContext()->where('title', 'individual')->update(['months' => 1]); parent::tearDown(); } /** * Test that turning wallet balance from negative to positive * unsuspends and undegrades the account */ public function testBalanceTurnsPositive(): void { Queue::fake(); $user = $this->getTestUser('jane@kolabnow.com'); $user->suspend(); $user->degrade(); $wallet = $user->wallets()->first(); $wallet->balance = -100; $wallet->save(); $this->assertTrue($user->isSuspended()); $this->assertTrue($user->isDegraded()); $this->assertNotNull($wallet->getSetting('balance_negative_since')); $wallet->balance = 100; $wallet->save(); $user->refresh(); $this->assertFalse($user->isSuspended()); $this->assertFalse($user->isDegraded()); $this->assertNull($wallet->getSetting('balance_negative_since')); // Test un-restricting users on balance change $owner = $this->getTestUser('UserWallet1@UserWallet.com'); $user1 = $this->getTestUser('UserWallet2@UserWallet.com'); $user2 = $this->getTestUser('UserWallet3@UserWallet.com'); $package = Package::withEnvTenantContext()->where('title', 'lite')->first(); $owner->assignPackage($package, $user1); $owner->assignPackage($package, $user2); $wallet = $owner->wallets()->first(); $owner->restrict(); $user1->restrict(); $user2->restrict(); $this->assertTrue($owner->isRestricted()); $this->assertTrue($user1->isRestricted()); $this->assertTrue($user2->isRestricted()); $this->fakeQueueReset(); $wallet->balance = 100; $wallet->save(); $this->assertFalse($owner->fresh()->isRestricted()); $this->assertFalse($user1->fresh()->isRestricted()); $this->assertFalse($user2->fresh()->isRestricted()); Queue::assertPushed(\App\Jobs\User\UpdateJob::class, 3); // TODO: Test group account and unsuspending domain/members/groups } /** * Test for Wallet::balanceLastsUntil() */ public function testBalanceLastsUntil(): void { // Monthly cost of all entitlements: 990 // 28 days: 35.36 per day // 31 days: 31.93 per day $user = $this->getTestUser('jane@kolabnow.com'); $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); $user->assignPlan($plan); $wallet = $user->wallets()->first(); // User/entitlements created today, balance=0 $until = $wallet->balanceLastsUntil(); $this->assertSame( Carbon::now()->addMonthsWithoutOverflow(1)->toDateString(), $until->toDateString() ); // User/entitlements created today, balance=-10 CHF $wallet->balance = -1000; $until = $wallet->balanceLastsUntil(); $this->assertSame(null, $until); // User/entitlements created today, balance=-9,99 CHF (monthly cost) $wallet->balance = 990; $until = $wallet->balanceLastsUntil(); $daysInLastMonth = \App\Utils::daysInLastMonth(); $delta = Carbon::now()->addMonthsWithoutOverflow(1)->addDays($daysInLastMonth)->diff($until)->days; $this->assertTrue($delta <= 1); $this->assertTrue($delta >= -1); // Old entitlements, 100% discount $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subDays(40)); $discount = \App\Discount::withEnvTenantContext()->where('discount', 100)->first(); $wallet->discount()->associate($discount); $until = $wallet->refresh()->balanceLastsUntil(); $this->assertSame(null, $until); // User with no entitlements $wallet->discount()->dissociate($discount); $wallet->entitlements()->delete(); $until = $wallet->refresh()->balanceLastsUntil(); $this->assertSame(null, $until); } /** * Basic wallet features */ public function testWallet(): void { // Verify a wallet is created, when a user is created. $user = $this->getTestUser('UserWallet1@UserWallet.com'); $this->assertCount(1, $user->wallets); $this->assertSame(\config('app.currency'), $user->wallets[0]->currency); $this->assertSame(0, $user->wallets[0]->balance); // Verify a user can haz more wallets. $user->wallets()->save(new Wallet(['currency' => 'USD'])); $user->refresh(); $this->assertCount(2, $user->wallets); $user->wallets()->each( function ($wallet) { $this->assertEquals(0, $wallet->balance); } ); // For now all wallets use system currency $this->assertFalse($user->wallets()->where('currency', 'USD')->exists()); // Verify we can not delete a user wallet that holds balance. $user->wallets()->each( function ($wallet) { $wallet->credit(100)->save(); } ); $user->wallets()->each( function ($wallet) { $this->assertFalse($wallet->delete()); } ); $user->wallets()->update(['balance' => 0]); $user->refresh(); // Verify we can remove a wallet that is an additional wallet. $user->wallets()->first()->delete(); $user->refresh(); $this->assertCount(1, $user->wallets); // Verify we can not delete a wallet that is the last wallet. $this->assertFalse($user->wallets[0]->delete()); } /** * Verify a wallet can be assigned a controller. */ public function testAddController(): void { $userA = $this->getTestUser('UserWallet1@UserWallet.com'); $userB = $this->getTestUser('UserWallet2@UserWallet.com'); $userA->wallets()->each( function ($wallet) use ($userB) { $wallet->addController($userB); } ); $this->assertCount(1, $userB->accounts); $aWallet = $userA->wallets()->first(); $bAccount = $userB->accounts()->first(); $this->assertTrue($bAccount->id === $aWallet->id); } /** * Test Wallet::expectedCharges() */ public function testExpectedCharges(): void { $user = $this->getTestUser('jane@kolabnow.com'); $package = Package::withEnvTenantContext()->where('title', 'kolab')->first(); $user->assignPackage($package); $wallet = $user->wallets->first(); // Verify the last day before the end of a full month's trial. $this->backdateEntitlements( $wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)->addDays(1) ); $this->assertEquals(0, $wallet->expectedCharges()); // Verify the exact end of the month's trial. $this->backdateEntitlements( $wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1) ); $this->assertEquals(990, $wallet->expectedCharges()); // Verify that over-running the trial by a single day causes charges to be incurred. $this->backdateEntitlements( $wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)->subDays(1) ); $this->assertEquals(990, $wallet->expectedCharges()); // Verify additional storage configuration entitlement created 'early' does incur additional // charges to the wallet. $this->backdateEntitlements( $wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)->subDays(1) ); $this->assertEquals(990, $wallet->expectedCharges()); $sku = Sku::withEnvTenantContext()->where('title', 'storage')->first(); $entitlement = Entitlement::create([ 'wallet_id' => $wallet->id, 'sku_id' => $sku->id, 'cost' => $sku->cost, 'entitleable_id' => $user->id, 'entitleable_type' => \App\User::class ]); $this->backdateEntitlements( [$entitlement], Carbon::now()->subMonthsWithoutOverflow(1)->subDays(1) ); $this->assertEquals(1015, $wallet->expectedCharges()); $entitlement->forceDelete(); $wallet->refresh(); // Verify additional storage configuration entitlement created 'late' does not incur additional // charges to the wallet. $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)); $this->assertEquals(990, $wallet->expectedCharges()); $entitlement = \App\Entitlement::create([ 'wallet_id' => $wallet->id, 'sku_id' => $sku->id, 'cost' => $sku->cost, 'entitleable_id' => $user->id, 'entitleable_type' => \App\User::class ]); $this->backdateEntitlements([$entitlement], Carbon::now()->subDays(14)); $this->assertEquals(990, $wallet->expectedCharges()); $entitlement->forceDelete(); $wallet->refresh(); // Test fifth week $targetDateA = Carbon::now()->subWeeks(5); $targetDateB = $targetDateA->copy()->addMonthsWithoutOverflow(1); $this->backdateEntitlements($wallet->entitlements, $targetDateA); $this->assertEquals(990, $wallet->expectedCharges()); $entitlement->forceDelete(); $wallet->refresh(); // Test second month $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(2)); $this->assertCount(7, $wallet->entitlements); $this->assertEquals(1980, $wallet->expectedCharges()); $entitlement = \App\Entitlement::create([ 'entitleable_id' => $user->id, 'entitleable_type' => \App\User::class, 'cost' => $sku->cost, 'sku_id' => $sku->id, 'wallet_id' => $wallet->id ]); $this->backdateEntitlements([$entitlement], Carbon::now()->subMonthsWithoutOverflow(1)); $this->assertEquals(2005, $wallet->expectedCharges()); $entitlement->forceDelete(); $wallet->refresh(); // Test cost calculation with a wallet discount $discount = Discount::withEnvTenantContext()->where('code', 'TEST')->first(); $wallet->discount()->associate($discount); $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(1)); $this->assertEquals(891, $wallet->expectedCharges()); } /** * Test Wallet::getMinMandateAmount() */ public function testGetMinMandateAmount(): void { $user = $this->getTestUser('UserWallet1@UserWallet.com'); $user->setSetting('plan_id', null); $wallet = $user->wallets()->first(); // No plan assigned $this->assertSame(Payment::MIN_AMOUNT, $wallet->getMinMandateAmount()); // Plan assigned $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); $plan->months = 12; $plan->save(); $user->setSetting('plan_id', $plan->id); $this->assertSame(990 * 12, $wallet->getMinMandateAmount()); // Plan and discount $discount = Discount::where('discount', 30)->first(); $wallet->discount()->associate($discount); $wallet->save(); $this->assertSame((int) (990 * 12 * 0.70), $wallet->getMinMandateAmount()); } /** * Test Wallet::isController() */ public function testIsController(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $wallet = $jack->wallet(); $this->assertTrue($wallet->isController($john)); $this->assertTrue($wallet->isController($ned)); $this->assertFalse($wallet->isController($jack)); } /** * Verify controllers can also be removed from wallets. */ public function testRemoveController(): void { $userA = $this->getTestUser('UserWallet1@UserWallet.com'); $userB = $this->getTestUser('UserWallet2@UserWallet.com'); $userA->wallets()->each( function ($wallet) use ($userB) { $wallet->addController($userB); } ); $userB->refresh(); $userB->accounts()->each( function ($wallet) use ($userB) { $wallet->removeController($userB); } ); $this->assertCount(0, $userB->accounts); } /** * Test for charging entitlements (including tenant commission calculations) */ public function testChargeEntitlements(): void { $user = $this->getTestUser('jane@kolabnow.com'); $discount = \App\Discount::withEnvTenantContext()->where('discount', 30)->first(); $wallet = $user->wallets()->first(); $wallet->discount()->associate($discount); + //Give the user enough balance so entitlements can be charged + $originalBalance = 1000; + $wallet->balance = $originalBalance; $wallet->save(); // Add 40% fee to all SKUs Sku::select()->update(['fee' => DB::raw("`cost` * 0.4")]); $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); $storage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); $mailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); $groupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first(); $user->assignPlan($plan); $user->assignSku($storage, 5); $user->setSetting('plan_id', null); // disable plan and trial // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month Carbon::setTestNow(Carbon::create(2021, 5, 21, 12)); // Add extra user with some deleted entitlements to make sure it does not interfere $otherUser = $this->getTestUser('UserWallet1@UserWallet.com'); $otherUser->assignPlan($plan); $otherUser->assignSku($storage, 5); $this->backdateEntitlements($otherUser->entitlements, Carbon::now()->subWeeks(7)); $otherUser->removeSku($storage, 2); $otherUser->entitlements()->withTrashed()->whereNotNull('deleted_at') ->update(['updated_at' => Carbon::now()->subWeeks(8)]); // Reset reseller's wallet balance and transactions $reseller_wallet = $user->tenant->wallet(); $reseller_wallet->balance = 0; $reseller_wallet->save(); $reseller_wallet->transactions()->delete(); // ------------------------------------------------ // Test skipping entitlements before a month passed // ------------------------------------------------ $backdate = Carbon::now()->subWeeks(3); $this->backdateEntitlements($user->entitlements, $backdate); // we expect no charges $this->assertSame(0, $wallet->chargeEntitlements()); - $this->assertSame(0, $wallet->balance); + $this->assertSame($originalBalance, $wallet->balance); $this->assertSame(0, $reseller_wallet->balance); $this->assertSame(0, $wallet->transactions()->count()); $this->assertSame(12, $user->entitlements()->where('updated_at', $backdate)->count()); // ------------------------------------ // Test normal charging of entitlements // ------------------------------------ // Backdate and charge entitlements, we're expecting one month to be charged $backdate = Carbon::now()->subWeeks(7); $this->backdateEntitlements($user->entitlements, $backdate); // Test with $apply=false argument $charge = $wallet->chargeEntitlements(false); $this->assertSame(778, $charge); - $this->assertSame(0, $wallet->balance); + $this->assertSame($originalBalance, $wallet->balance); $this->assertSame(0, $wallet->transactions()->count()); $charge = $wallet->chargeEntitlements(); $wallet->refresh(); $reseller_wallet->refresh(); // User discount is 30% // Expected: groupware: floor(490 * 70%) + mailbox: floor(500 * 70%) + storage: 5 * floor(25 * 70%) = 778 $this->assertSame(778, $charge); - $this->assertSame(-778, $wallet->balance); + $this->assertSame($originalBalance - $charge, $wallet->balance); // Reseller fee is 40% // Expected: 778 - groupware: floor(490 * 40%) - mailbox: floor(500 * 40%) - storage: 5 * floor(25 * 40%) = 332 $this->assertSame(332, $reseller_wallet->balance); $transactions = $wallet->transactions()->get(); $this->assertCount(1, $transactions); $trans = $transactions[0]; $this->assertSame('', $trans->description); $this->assertSame(-778, $trans->amount); $this->assertSame(Transaction::WALLET_DEBIT, $trans->type); $reseller_transactions = $reseller_wallet->transactions()->get(); $this->assertCount(1, $reseller_transactions); $trans = $reseller_transactions[0]; $this->assertSame("Charged user jane@kolabnow.com", $trans->description); $this->assertSame(332, $trans->amount); $this->assertSame(Transaction::WALLET_CREDIT, $trans->type); // Assert all entitlements' updated_at timestamp $date = $backdate->addMonthsWithoutOverflow(1); $this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get()); // Assert per-entitlement transactions $entitlement_transactions = Transaction::where('transaction_id', $transactions[0]->id) ->where('type', Transaction::ENTITLEMENT_BILLED) ->get(); $this->assertSame(7, $entitlement_transactions->count()); $this->assertSame(778, $entitlement_transactions->sum('amount')); $groupware_entitlement = $user->entitlements->where('sku_id', '===', $groupware->id)->first(); $mailbox_entitlement = $user->entitlements->where('sku_id', '===', $mailbox->id)->first(); $this->assertSame(1, $entitlement_transactions->where('object_id', $groupware_entitlement->id)->count()); $this->assertSame(1, $entitlement_transactions->where('object_id', $mailbox_entitlement->id)->count()); $excludes = [$mailbox_entitlement->id, $groupware_entitlement->id]; $this->assertSame(5, $entitlement_transactions->whereNotIn('object_id', $excludes)->count()); // ----------------------------------- // Test charging deleted entitlements // ----------------------------------- - $wallet->balance = 0; + $originalBalance = 1000; + $wallet->balance = $originalBalance; $wallet->save(); $wallet->transactions()->delete(); $reseller_wallet->balance = 0; $reseller_wallet->save(); $reseller_wallet->transactions()->delete(); $user->removeSku($storage, 2); // we expect the wallet to have been charged for 19 days of use of 2 deleted storage entitlements $charge = $wallet->chargeEntitlements(); $wallet->refresh(); $reseller_wallet->refresh(); // 2 * floor(25 / 31 * 70% * 19) = 20 $this->assertSame(20, $charge); - $this->assertSame(-20, $wallet->balance); + $this->assertSame($originalBalance - $charge, $wallet->balance); // 20 - 2 * floor(25 / 31 * 40% * 19) = 8 $this->assertSame(8, $reseller_wallet->balance); $transactions = $wallet->transactions()->get(); $this->assertCount(1, $transactions); $trans = $transactions[0]; $this->assertSame('', $trans->description); $this->assertSame(-20, $trans->amount); $this->assertSame(Transaction::WALLET_DEBIT, $trans->type); $reseller_transactions = $reseller_wallet->transactions()->get(); $this->assertCount(1, $reseller_transactions); $trans = $reseller_transactions[0]; $this->assertSame("Charged user jane@kolabnow.com", $trans->description); $this->assertSame(8, $trans->amount); $this->assertSame(Transaction::WALLET_CREDIT, $trans->type); // Assert per-entitlement transactions $entitlement_transactions = Transaction::where('transaction_id', $transactions[0]->id) ->where('type', Transaction::ENTITLEMENT_BILLED) ->get(); $storage_entitlements = $user->entitlements->where('sku_id', $storage->id)->where('cost', '>', 0)->pluck('id'); $this->assertSame(2, $entitlement_transactions->count()); $this->assertSame(20, $entitlement_transactions->sum('amount')); $this->assertSame(2, $entitlement_transactions->whereIn('object_id', $storage_entitlements)->count()); // -------------------------------------------------- // Test skipping deleted entitlements already charged // -------------------------------------------------- $wallet->balance = 0; $wallet->save(); $wallet->transactions()->delete(); $reseller_wallet->balance = 0; $reseller_wallet->save(); $reseller_wallet->transactions()->delete(); // we expect no charges $this->assertSame(0, $wallet->chargeEntitlements()); $this->assertSame(0, $wallet->balance); $this->assertSame(0, $wallet->transactions()->count()); $this->assertSame(0, $reseller_wallet->fresh()->balance); // --------------------------------------------------------- // Test (not) charging entitlements deleted before 14 days // --------------------------------------------------------- $backdate = Carbon::now()->subDays(13); $ent = $user->entitlements->where('sku_id', $groupware->id)->first(); Entitlement::where('id', $ent->id)->update([ 'created_at' => $backdate, 'updated_at' => $backdate, 'deleted_at' => Carbon::now(), ]); // we expect no charges $this->assertSame(0, $wallet->chargeEntitlements()); $this->assertSame(0, $wallet->balance); $this->assertSame(0, $wallet->transactions()->count()); $this->assertSame(0, $reseller_wallet->fresh()->balance); // expect update of updated_at timestamp $this->assertSame(Carbon::now()->toDateTimeString(), $ent->fresh()->updated_at->toDateTimeString()); // ------------------------------------------------------- // Test charging a degraded account // Test both deleted and non-deleted in the same operation // ------------------------------------------------------- // At this point user has: mailbox + 8 x storage $backdate = Carbon::now()->subWeeks(7); $this->backdateEntitlements($user->entitlements->fresh(), $backdate); $user->status |= User::STATUS_DEGRADED; $user->saveQuietly(); $wallet->refresh(); - $wallet->balance = 0; + $originalBalance = 1000; + $wallet->balance = $originalBalance; $wallet->save(); $reseller_wallet->balance = 0; $reseller_wallet->save(); Transaction::truncate(); $charge = $wallet->chargeEntitlements(); $reseller_wallet->refresh(); // User would be charged if not degraded: mailbox: floor(500 * 70%) + storage: 3 * floor(25 * 70%) = 401 $this->assertSame(0, $charge); - $this->assertSame(0, $wallet->balance); + $this->assertSame($originalBalance, $wallet->balance); // Expected: 0 - mailbox: floor(500 * 40%) - storage: 3 * floor(25 * 40%) = -230 - $this->assertSame(-230, $reseller_wallet->balance); + $this->assertSame(0, $reseller_wallet->balance); // Assert all entitlements' updated_at timestamp $date = $backdate->addMonthsWithoutOverflow(1); $this->assertSame(9, $wallet->entitlements()->where('updated_at', $date)->count()); // There should be only one transaction at this point (for the reseller wallet) - $this->assertSame(1, Transaction::count()); + $this->assertSame(0, Transaction::count()); } /** * Test for charging entitlements when in trial */ public function testChargeEntitlementsTrial(): void { $user = $this->getTestUser('jane@kolabnow.com'); $wallet = $user->wallets()->first(); + $originalBalance = 1000; + $wallet->balance = $originalBalance; + $wallet->save(); // Add 40% fee to all SKUs Sku::select()->update(['fee' => DB::raw("`cost` * 0.4")]); $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); $storage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); $user->assignPlan($plan); $user->assignSku($storage, 5); // Reset reseller's wallet balance and transactions $reseller_wallet = $user->tenant->wallet(); $reseller_wallet->balance = 0; $reseller_wallet->save(); $reseller_wallet->transactions()->delete(); // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month Carbon::setTestNow(Carbon::create(2021, 5, 21, 12)); // ------------------------------------ // Test normal charging of entitlements // ------------------------------------ // Backdate and charge entitlements, we're expecting one month to be charged $backdate = Carbon::now()->subWeeks(7); // 2021-04-02 $this->backdateEntitlements($user->entitlements, $backdate, $backdate); $charge = $wallet->chargeEntitlements(); $reseller_wallet->refresh(); // Expected: storage: 5 * 25 = 125 (the rest is free in trial) - $this->assertSame($balance = -125, $wallet->balance); - $this->assertSame(-$balance, $charge); + $this->assertSame(125, $charge); + $this->assertSame($originalBalance - $charge, $wallet->balance); // Reseller fee is 40% // Expected: 125 - 5 * floor(25 * 40%) = 75 $this->assertSame($reseller_balance = 75, $reseller_wallet->balance); // Assert wallet transaction $transactions = $wallet->transactions()->get(); $this->assertCount(1, $transactions); $trans = $transactions[0]; $this->assertSame('', $trans->description); - $this->assertSame($balance, $trans->amount); + $this->assertSame(-$charge, $trans->amount); $this->assertSame(Transaction::WALLET_DEBIT, $trans->type); // Assert entitlement transactions $etransactions = Transaction::where('transaction_id', $trans->id)->get(); $this->assertCount(5, $etransactions); $trans = $etransactions[0]; $this->assertSame(null, $trans->description); $this->assertSame(25, $trans->amount); $this->assertSame(Transaction::ENTITLEMENT_BILLED, $trans->type); // Assert all entitlements' updated_at timestamp $date = $backdate->addMonthsWithoutOverflow(1); - $this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get()); + //TODO why? + $this->assertCount(10, $wallet->entitlements()->where('updated_at', $date)->get()); // Run again, expect no changes $charge = $wallet->chargeEntitlements(); $wallet->refresh(); $this->assertSame(0, $charge); - $this->assertSame($balance, $wallet->balance); + $this->assertSame(875, $wallet->balance); $this->assertCount(1, $wallet->transactions()->get()); - $this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get()); + $this->assertCount(10, $wallet->entitlements()->where('updated_at', $date)->get()); // ----------------------------------- // Test charging deleted entitlements // ----------------------------------- - $wallet->balance = 0; + $originalBalance = 1000; + $wallet->balance = $originalBalance; $wallet->save(); $reseller_wallet->balance = 0; $reseller_wallet->save(); Transaction::truncate(); $user->removeSku($storage, 2); $charge = $wallet->chargeEntitlements(); $wallet->refresh(); $reseller_wallet->refresh(); // we expect the wallet to have been charged for 19 days of use of // 2 deleted storage entitlements: 2 * floor(25 / 31 * 19) = 30 - $this->assertSame(-30, $wallet->balance); $this->assertSame(30, $charge); + $this->assertSame($originalBalance - $charge, $wallet->balance); // Reseller fee is 40% // Expected: 30 - 2 * floor(25 / 31 * 40% * 19) = 18 $this->assertSame(18, $reseller_wallet->balance); // Assert wallet transactions $transactions = $wallet->transactions()->get(); $this->assertCount(1, $transactions); $trans = $transactions[0]; $this->assertSame('', $trans->description); $this->assertSame(-30, $trans->amount); $this->assertSame(Transaction::WALLET_DEBIT, $trans->type); // Assert entitlement transactions $etransactions = Transaction::where('transaction_id', $trans->id)->get(); $this->assertCount(2, $etransactions); $trans = $etransactions[0]; $this->assertSame(null, $trans->description); $this->assertSame(15, $trans->amount); $this->assertSame(Transaction::ENTITLEMENT_BILLED, $trans->type); // Assert the deleted entitlements' updated_at timestamp was bumped $this->assertSame(2, $wallet->entitlements()->withTrashed()->whereColumn('updated_at', 'deleted_at')->count()); // TODO: Test a case when trial ends after the entitlement deletion date } /** * Tests for award/penalty/chargeback/refund/credit/debit methods */ public function testBalanceChange(): void { $user = $this->getTestUser('UserWallet1@UserWallet.com'); $wallet = $user->wallets()->first(); // Test award $this->assertSame($wallet->id, $wallet->award(100, 'test')->id); $this->assertSame(100, $wallet->balance); $this->assertSame(100, $wallet->fresh()->balance); $transaction = $wallet->transactions()->first(); $this->assertSame(100, $transaction->amount); $this->assertSame(Transaction::WALLET_AWARD, $transaction->type); $this->assertSame('test', $transaction->description); $wallet->transactions()->delete(); // Test penalty $this->assertSame($wallet->id, $wallet->penalty(100, 'test')->id); $this->assertSame(0, $wallet->balance); $this->assertSame(0, $wallet->fresh()->balance); $transaction = $wallet->transactions()->first(); $this->assertSame(-100, $transaction->amount); $this->assertSame(Transaction::WALLET_PENALTY, $transaction->type); $this->assertSame('test', $transaction->description); $wallet->transactions()->delete(); $wallet->balance = 0; $wallet->save(); // Test chargeback $this->assertSame($wallet->id, $wallet->chargeback(100, 'test')->id); $this->assertSame(-100, $wallet->balance); $this->assertSame(-100, $wallet->fresh()->balance); $transaction = $wallet->transactions()->first(); $this->assertSame(-100, $transaction->amount); $this->assertSame(Transaction::WALLET_CHARGEBACK, $transaction->type); $this->assertSame('test', $transaction->description); $wallet->transactions()->delete(); $wallet->balance = 0; $wallet->save(); // Test refund $this->assertSame($wallet->id, $wallet->refund(100, 'test')->id); $this->assertSame(-100, $wallet->balance); $this->assertSame(-100, $wallet->fresh()->balance); $transaction = $wallet->transactions()->first(); $this->assertSame(-100, $transaction->amount); $this->assertSame(Transaction::WALLET_REFUND, $transaction->type); $this->assertSame('test', $transaction->description); $wallet->transactions()->delete(); $wallet->balance = 0; $wallet->save(); // Test credit $this->assertSame($wallet->id, $wallet->credit(100, 'test')->id); $this->assertSame(100, $wallet->balance); $this->assertSame(100, $wallet->fresh()->balance); $transaction = $wallet->transactions()->first(); $this->assertSame(100, $transaction->amount); $this->assertSame(Transaction::WALLET_CREDIT, $transaction->type); $this->assertSame('test', $transaction->description); $wallet->transactions()->delete(); $wallet->balance = 0; $wallet->save(); // Test debit $this->assertSame($wallet->id, $wallet->debit(100, 'test')->id); $this->assertSame(-100, $wallet->balance); $this->assertSame(-100, $wallet->fresh()->balance); $transaction = $wallet->transactions()->first(); $this->assertSame(-100, $transaction->amount); $this->assertSame(Transaction::WALLET_DEBIT, $transaction->type); $this->assertSame('test', $transaction->description); } /** * Tests for updateEntitlements() */ public function testUpdateEntitlements(): void { $user = $this->getTestUser('jane@kolabnow.com'); $discount = \App\Discount::withEnvTenantContext()->where('discount', 30)->first(); $wallet = $user->wallets()->first(); $wallet->discount()->associate($discount); $wallet->save(); // Add 40% fee to all SKUs Sku::select()->update(['fee' => DB::raw("`cost` * 0.4")]); $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); $storage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); $mailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); $groupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first(); $user->assignPlan($plan); $user->setSetting('plan_id', null); // disable plan and trial // Reset reseller's wallet balance and transactions $reseller_wallet = $user->tenant->wallet(); $reseller_wallet->balance = 0; $reseller_wallet->save(); $reseller_wallet->transactions()->delete(); // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month Carbon::setTestNow(Carbon::create(2021, 5, 21, 12)); $now = Carbon::now(); // Backdate and charge entitlements $backdate = Carbon::now()->subWeeks(3)->setHour(10); $this->backdateEntitlements($user->entitlements, $backdate); // --------------------------------------- // Update entitlements with no cost charge // --------------------------------------- // Test with $withCost=false argument $charge = $wallet->updateEntitlements(false); $wallet->refresh(); $reseller_wallet->refresh(); $this->assertSame(0, $charge); $this->assertSame(0, $wallet->balance); $this->assertSame(0, $wallet->transactions()->count()); // Expected: 0 - groupware: floor(490 / 31 * 21 * 40%) - mailbox: floor(500 / 31 * 21 * 40%) = -267 - $this->assertSame(-267, $reseller_wallet->balance); + $this->assertSame(0, $reseller_wallet->balance); // Assert all entitlements' updated_at timestamp $date = $now->copy()->setTimeFrom($backdate); $this->assertCount(7, $wallet->entitlements()->where('updated_at', $date)->get()); $reseller_transactions = $reseller_wallet->transactions()->get(); - $this->assertCount(1, $reseller_transactions); - $trans = $reseller_transactions[0]; - $this->assertSame("Charged user jane@kolabnow.com", $trans->description); - $this->assertSame(-267, $trans->amount); - $this->assertSame(Transaction::WALLET_DEBIT, $trans->type); + $this->assertCount(0, $reseller_transactions); // ------------------------------------ // Update entitlements with cost charge // ------------------------------------ $reseller_wallet = $user->tenant->wallet(); $reseller_wallet->balance = 0; $reseller_wallet->save(); $reseller_wallet->transactions()->delete(); $this->backdateEntitlements($user->entitlements, $backdate); $charge = $wallet->updateEntitlements(); $wallet->refresh(); $reseller_wallet->refresh(); // User discount is 30% // Expected: groupware: floor(490 / 31 * 21 * 70%) + mailbox: floor(500 / 31 * 21 * 70%) = 469 $this->assertSame(469, $charge); $this->assertSame(-469, $wallet->balance); // Reseller fee is 40% + //FIXME is this correcct? // Expected: 469 - groupware: floor(490 / 31 * 21 * 40%) - mailbox: floor(500 / 31 * 21 * 40%) = 202 - $this->assertSame(202, $reseller_wallet->balance); + $this->assertSame(-267, $reseller_wallet->balance); $transactions = $wallet->transactions()->get(); $this->assertCount(1, $transactions); $trans = $transactions[0]; $this->assertSame('', $trans->description); $this->assertSame(-469, $trans->amount); $this->assertSame(Transaction::WALLET_DEBIT, $trans->type); $reseller_transactions = $reseller_wallet->transactions()->get(); $this->assertCount(1, $reseller_transactions); $trans = $reseller_transactions[0]; $this->assertSame("Charged user jane@kolabnow.com", $trans->description); - $this->assertSame(202, $trans->amount); - $this->assertSame(Transaction::WALLET_CREDIT, $trans->type); + //FIXME is this correcct? + $this->assertSame(-267, $trans->amount); + $this->assertSame(Transaction::WALLET_DEBIT, $trans->type); // Assert all entitlements' updated_at timestamp $date = $now->copy()->setTimeFrom($backdate); $this->assertCount(7, $wallet->entitlements()->where('updated_at', $date)->get()); // Assert per-entitlement transactions $groupware_entitlement = $user->entitlements->where('sku_id', '===', $groupware->id)->first(); $mailbox_entitlement = $user->entitlements->where('sku_id', '===', $mailbox->id)->first(); $entitlement_transactions = Transaction::where('transaction_id', $transactions[0]->id) ->where('type', Transaction::ENTITLEMENT_BILLED) ->get(); $this->assertSame(2, $entitlement_transactions->count()); $this->assertSame(469, $entitlement_transactions->sum('amount')); $this->assertSame(1, $entitlement_transactions->where('object_id', $groupware_entitlement->id)->count()); $this->assertSame(1, $entitlement_transactions->where('object_id', $mailbox_entitlement->id)->count()); } /** * Tests for vatRate() */ public function testVatRate(): void { $rate1 = VatRate::create([ 'start' => now()->subDay(), 'country' => 'US', 'rate' => 7.5, ]); $rate2 = VatRate::create([ 'start' => now()->subDay(), 'country' => 'DE', 'rate' => 10.0, ]); $user = $this->getTestUser('UserWallet1@UserWallet.com'); $wallet = $user->wallets()->first(); $user->setSetting('country', null); $this->assertSame(null, $wallet->vatRate()); $user->setSetting('country', 'PL'); $this->assertSame(null, $wallet->vatRate()); $user->setSetting('country', 'US'); $this->assertSame($rate1->id, $wallet->vatRate()->id); // @phpstan-ignore-line $user->setSetting('country', 'DE'); $this->assertSame($rate2->id, $wallet->vatRate()->id); // @phpstan-ignore-line // Test $start argument $rate3 = VatRate::create([ 'start' => now()->subYear(), 'country' => 'DE', 'rate' => 5.0, ]); $this->assertSame($rate2->id, $wallet->vatRate()->id); // @phpstan-ignore-line $this->assertSame($rate3->id, $wallet->vatRate(now()->subMonth())->id); $this->assertSame(null, $wallet->vatRate(now()->subYears(2))); } }