diff --git a/src/tests/Feature/Stories/GreylistTest.php b/src/tests/Feature/Stories/GreylistTest.php index b5bb811b..22ba7428 100644 --- a/src/tests/Feature/Stories/GreylistTest.php +++ b/src/tests/Feature/Stories/GreylistTest.php @@ -1,617 +1,630 @@ setUpTest(); $this->useServicesUrl(); $this->instance = $this->generateInstanceId(); $this->clientAddress = '212.103.80.148'; $this->net = \App\IP4Net::getNet($this->clientAddress); DB::delete("DELETE FROM greylist_connect WHERE sender_domain = 'sender.domain';"); DB::delete("DELETE FROM greylist_settings;"); DB::delete("DELETE FROM greylist_whitelist WHERE sender_domain = 'sender.domain';"); } public function tearDown(): void { DB::delete("DELETE FROM greylist_connect WHERE sender_domain = 'sender.domain';"); DB::delete("DELETE FROM greylist_settings;"); DB::delete("DELETE FROM greylist_whitelist WHERE sender_domain = 'sender.domain';"); parent::tearDown(); } public function testWithTimestamp() { $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(7)->toString() ] ); $timestamp = $this->getObjectProperty($request, 'timestamp'); $this->assertTrue( \Carbon\Carbon::parse($timestamp, 'UTC') < \Carbon\Carbon::now() ); } public function testNoNet() { $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => '127.128.129.130', 'client_name' => 'some.mx' ] ); $this->assertTrue($request->shouldDefer()); } public function testIp6Net() { $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => '2a00:1450:400a:803::2005', 'client_name' => 'some.mx' ] ); $this->assertTrue($request->shouldDefer()); } // public function testMultiRecipientThroughAlias() {} public function testWhitelistNew() { $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); $this->assertNull($whitelist); for ($i = 0; $i < 5; $i++) { $request = new Greylist\Request( [ 'sender' => "someone{$i}@sender.domain", 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(1) ] ); $this->assertTrue($request->shouldDefer()); } $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); $this->assertNotNull($whitelist); $request = new Greylist\Request( [ 'sender' => "someone5@sender.domain", 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(1) ] ); $this->assertFalse($request->shouldDefer()); } // public function testWhitelistedHit() {} public function testWhitelistStale() { $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); $this->assertNull($whitelist); for ($i = 0; $i < 5; $i++) { $request = new Greylist\Request( [ 'sender' => "someone{$i}@sender.domain", 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(1) ] ); $this->assertTrue($request->shouldDefer()); } $whitelist = Greylist\Whitelist::where('sender_domain', 'sender.domain')->first(); $this->assertNotNull($whitelist); $request = new Greylist\Request( [ 'sender' => "someone5@sender.domain", 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx', 'timestamp' => \Carbon\Carbon::now()->subDays(1) ] ); $this->assertFalse($request->shouldDefer()); $whitelist->updated_at = \Carbon\Carbon::now()->subMonthsWithoutOverflow(2); $whitelist->save(['timestamps' => false]); $this->assertTrue($request->shouldDefer()); } // public function testWhitelistUpdate() {} public function testNew() { $data = [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress, 'client_name' => 'some.mx' ]; $response = $this->post('/api/webhooks/policy/greylist', $data); $response->assertStatus(403); } public function testRetry() { $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); $connect->created_at = \Carbon\Carbon::now()->subMinutes(6); $connect->save(); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } public function testDomainDisabled() { $setting = Greylist\Setting::create( [ 'object_id' => $this->domainHosted->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } public function testDomainEnabled() { $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => \App\IP4Net::getNet('212.103.80.148')->id, 'net_type' => \App\IP4Net::class ] ); $setting = Greylist\Setting::create( [ 'object_id' => $this->domainHosted->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled', 'value' => 'true' ] ); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); $connect->created_at = \Carbon\Carbon::now()->subMinutes(6); $connect->save(); $this->assertFalse($request->shouldDefer()); } public function testDomainDisabledUserDisabled() { $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); $settingDomain = Greylist\Setting::create( [ 'object_id' => $this->domainHosted->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); $settingUser = Greylist\Setting::create( [ 'object_id' => $this->domainOwner->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } public function testDomainDisabledUserEnabled() { $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); $settingDomain = Greylist\Setting::create( [ 'object_id' => $this->domainHosted->id, 'object_type' => \App\Domain::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); $settingUser = Greylist\Setting::create( [ 'object_id' => $this->domainOwner->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'true' ] ); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); $connect->created_at = \Carbon\Carbon::now()->subMinutes(6); $connect->save(); $this->assertFalse($request->shouldDefer()); } public function testInvalidDomain() { $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => 1234, 'recipient_type' => \App\Domain::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => 'not.someone@that.exists', 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); } public function testInvalidUser() { $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => 1234, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => 'not.someone@that.exists', 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); } public function testUserDisabled() { $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); $setting = Greylist\Setting::create( [ 'object_id' => $this->domainOwner->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); + + + // Ensure we also find the setting by alias + $aliases = $this->domainOwner->aliases()->orderBy('alias')->get(); + $request = new Greylist\Request( + [ + 'sender' => 'someone@sender.domain', + 'recipient' => $aliases[0]->alias, + 'client_address' => $this->clientAddress + ] + ); + + $this->assertFalse($request->shouldDefer()); } public function testUserEnabled() { $connect = Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $this->domainOwner->email), 'recipient_id' => $this->domainOwner->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); $setting = Greylist\Setting::create( [ 'object_id' => $this->domainOwner->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'true' ] ); $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); $this->assertTrue($request->shouldDefer()); $connect->created_at = \Carbon\Carbon::now()->subMinutes(6); $connect->save(); $this->assertFalse($request->shouldDefer()); } public function testMultipleUsersAllDisabled() { $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); foreach ($this->domainUsers as $user) { Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $user->email), 'recipient_id' => $user->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); Greylist\Setting::create( [ 'object_id' => $user->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => 'false' ] ); if ($user->email == $this->domainOwner->email) { continue; } $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $user->email, 'client_address' => $this->clientAddress ] ); $this->assertFalse($request->shouldDefer()); } } public function testMultipleUsersAnyEnabled() { $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $this->domainOwner->email, 'client_address' => $this->clientAddress ] ); foreach ($this->domainUsers as $user) { Greylist\Connect::create( [ 'sender_local' => 'someone', 'sender_domain' => 'sender.domain', 'recipient_hash' => hash('sha256', $user->email), 'recipient_id' => $user->id, 'recipient_type' => \App\User::class, 'connect_count' => 1, 'net_id' => $this->net->id, 'net_type' => \App\IP4Net::class ] ); Greylist\Setting::create( [ 'object_id' => $user->id, 'object_type' => \App\User::class, 'key' => 'greylist_enabled', 'value' => ($user->id == $this->jack->id) ? 'true' : 'false' ] ); if ($user->email == $this->domainOwner->email) { continue; } $request = new Greylist\Request( [ 'sender' => 'someone@sender.domain', 'recipient' => $user->email, 'client_address' => $this->clientAddress ] ); if ($user->id == $this->jack->id) { $this->assertTrue($request->shouldDefer()); } else { $this->assertFalse($request->shouldDefer()); } } } private function generateInstanceId() { $instance = []; for ($x = 0; $x < 3; $x++) { for ($y = 0; $y < 3; $y++) { $instance[] = substr('01234567889', rand(0, 9), 1); } } return implode('.', $instance); } } diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php index badac263..147b0953 100644 --- a/src/tests/TestCaseTrait.php +++ b/src/tests/TestCaseTrait.php @@ -1,470 +1,479 @@ 'John', 'last_name' => 'Doe', 'organization' => 'Test Domain Owner', ]; /** * Some users for the hosted domain, ultimately including the owner. * * @var \App\User[] */ protected $domainUsers = []; /** * A specific user that is a regular user in the hosted domain. * * @var ?\App\User */ protected $jack; /** * A specific user that is a controller on the wallet to which the hosted domain is charged. * * @var ?\App\User */ protected $jane; /** * A specific user that has a second factor configured. * * @var ?\App\User */ protected $joe; /** * One of the domains that is available for public registration. * * @var ?\App\Domain */ protected $publicDomain; /** * A newly generated user in a public domain. * * @var ?\App\User */ protected $publicDomainUser; /** * A placeholder for a password that can be generated. * * Should be generated with `\App\Utils::generatePassphrase()`. * * @var ?string */ protected $userPassword; /** * Assert that the entitlements for the user match the expected list of entitlements. * * @param \App\User $user The user for which the entitlements need to be pulled. * @param array $expected An array of expected \App\SKU titles. */ protected function assertUserEntitlements($user, $expected) { // Assert the user entitlements $skus = $user->entitlements()->get() ->map(function ($ent) { return $ent->sku->title; }) ->toArray(); sort($skus); Assert::assertSame($expected, $skus); } protected function backdateEntitlements($entitlements, $targetDate) { $wallets = []; $ids = []; foreach ($entitlements as $entitlement) { $ids[] = $entitlement->id; $wallets[] = $entitlement->wallet_id; } \App\Entitlement::whereIn('id', $ids)->update([ 'created_at' => $targetDate, 'updated_at' => $targetDate, ]); if (!empty($wallets)) { $wallets = array_unique($wallets); $owners = \App\Wallet::whereIn('id', $wallets)->pluck('user_id')->all(); \App\User::whereIn('id', $owners)->update(['created_at' => $targetDate]); } } /** * Removes all beta entitlements from the database */ protected function clearBetaEntitlements(): void { $beta_handlers = [ 'App\Handlers\Beta', 'App\Handlers\Distlist', ]; $betas = \App\Sku::whereIn('handler_class', $beta_handlers)->pluck('id')->all(); \App\Entitlement::whereIn('sku_id', $betas)->delete(); } /** * Creates the application. * * @return \Illuminate\Foundation\Application */ public function createApplication() { $app = require __DIR__ . '/../bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); return $app; } /** * Create a set of transaction log entries for a wallet */ protected function createTestTransactions($wallet) { $result = []; $date = Carbon::now(); $debit = 0; $entitlementTransactions = []; foreach ($wallet->entitlements as $entitlement) { if ($entitlement->cost) { $debit += $entitlement->cost; $entitlementTransactions[] = $entitlement->createTransaction( Transaction::ENTITLEMENT_BILLED, $entitlement->cost ); } } $transaction = Transaction::create( [ 'user_email' => 'jeroen@jeroen.jeroen', 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => Transaction::WALLET_DEBIT, 'amount' => $debit * -1, 'description' => 'Payment', ] ); $result[] = $transaction; Transaction::whereIn('id', $entitlementTransactions)->update(['transaction_id' => $transaction->id]); $transaction = Transaction::create( [ 'user_email' => null, 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => Transaction::WALLET_CREDIT, 'amount' => 2000, 'description' => 'Payment', ] ); $transaction->created_at = $date->next(Carbon::MONDAY); $transaction->save(); $result[] = $transaction; $types = [ Transaction::WALLET_AWARD, Transaction::WALLET_PENALTY, ]; // The page size is 10, so we generate so many to have at least two pages $loops = 10; while ($loops-- > 0) { $type = $types[count($result) % count($types)]; $transaction = Transaction::create([ 'user_email' => 'jeroen.@jeroen.jeroen', 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => $type, 'amount' => 11 * (count($result) + 1) * ($type == Transaction::WALLET_PENALTY ? -1 : 1), 'description' => 'TRANS' . $loops, ]); $transaction->created_at = $date->next(Carbon::MONDAY); $transaction->save(); $result[] = $transaction; } return $result; } /** * Delete a test domain whatever it takes. * * @coversNothing */ protected function deleteTestDomain($name) { Queue::fake(); $domain = Domain::withTrashed()->where('namespace', $name)->first(); if (!$domain) { return; } $job = new \App\Jobs\Domain\DeleteJob($domain->id); $job->handle(); $domain->forceDelete(); } /** * Delete a test group whatever it takes. * * @coversNothing */ protected function deleteTestGroup($email) { Queue::fake(); $group = Group::withTrashed()->where('email', $email)->first(); if (!$group) { return; } $job = new \App\Jobs\Group\DeleteJob($group->id); $job->handle(); $group->forceDelete(); } /** * Delete a test user whatever it takes. * * @coversNothing */ protected function deleteTestUser($email) { Queue::fake(); $user = User::withTrashed()->where('email', $email)->first(); if (!$user) { return; } $job = new \App\Jobs\User\DeleteJob($user->id); $job->handle(); $user->forceDelete(); } /** * Helper to access protected property of an object */ protected static function getObjectProperty($object, $property_name) { $reflection = new \ReflectionClass($object); $property = $reflection->getProperty($property_name); $property->setAccessible(true); return $property->getValue($object); } /** * Get Domain object by namespace, create it if needed. * Skip LDAP jobs. * * @coversNothing */ protected function getTestDomain($name, $attrib = []) { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); return Domain::firstOrCreate(['namespace' => $name], $attrib); } /** * Get Group object by email, create it if needed. * Skip LDAP jobs. */ protected function getTestGroup($email, $attrib = []) { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); return Group::firstOrCreate(['email' => $email], $attrib); } /** * Get User object by email, create it if needed. * Skip LDAP jobs. * * @coversNothing */ protected function getTestUser($email, $attrib = []) { // Disable jobs (i.e. skip LDAP oprations) Queue::fake(); $user = User::firstOrCreate(['email' => $email], $attrib); if ($user->trashed()) { // Note: we do not want to use user restore here User::where('id', $user->id)->forceDelete(); $user = User::create(['email' => $email] + $attrib); } return $user; } /** * Call protected/private method of a class. * * @param object $object Instantiated object that we will run method on. * @param string $methodName Method name to call * @param array $parameters Array of parameters to pass into method. * * @return mixed Method return. */ protected function invokeMethod($object, $methodName, array $parameters = array()) { $reflection = new \ReflectionClass(get_class($object)); $method = $reflection->getMethod($methodName); $method->setAccessible(true); return $method->invokeArgs($object, $parameters); } protected function setUpTest() { $this->userPassword = \App\Utils::generatePassphrase(); $this->domainHosted = $this->getTestDomain( 'test.domain', [ 'type' => \App\Domain::TYPE_EXTERNAL, 'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED ] ); + $this->getTestDomain( + 'test2.domain2', + [ + 'type' => \App\Domain::TYPE_EXTERNAL, + 'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED + ] + ); + $packageKolab = \App\Package::where('title', 'kolab')->first(); $this->domainOwner = $this->getTestUser('john@test.domain', ['password' => $this->userPassword]); $this->domainOwner->assignPackage($packageKolab); $this->domainOwner->setSettings($this->domainOwnerSettings); + $this->domainOwner->setAliases(['alias1@test2.domain2']); // separate for regular user $this->jack = $this->getTestUser('jack@test.domain', ['password' => $this->userPassword]); // separate for wallet controller $this->jane = $this->getTestUser('jane@test.domain', ['password' => $this->userPassword]); $this->joe = $this->getTestUser('joe@test.domain', ['password' => $this->userPassword]); $this->domainUsers[] = $this->jack; $this->domainUsers[] = $this->jane; $this->domainUsers[] = $this->joe; $this->domainUsers[] = $this->getTestUser('jill@test.domain', ['password' => $this->userPassword]); foreach ($this->domainUsers as $user) { $this->domainOwner->assignPackage($packageKolab, $user); } $this->domainUsers[] = $this->domainOwner; // assign second factor to joe $this->joe->assignSku(\App\Sku::where('title', '2fa')->first()); \App\Auth\SecondFactor::seed($this->joe->email); usort( $this->domainUsers, function ($a, $b) { return $a->email > $b->email; } ); $this->domainHosted->assignPackage( \App\Package::where('title', 'domain-hosting')->first(), $this->domainOwner ); $wallet = $this->domainOwner->wallets()->first(); $wallet->addController($this->jane); $this->publicDomain = \App\Domain::where('type', \App\Domain::TYPE_PUBLIC)->first(); $this->publicDomainUser = $this->getTestUser( 'john@' . $this->publicDomain->namespace, ['password' => $this->userPassword] ); $this->publicDomainUser->assignPackage($packageKolab); } public function tearDown(): void { foreach ($this->domainUsers as $user) { if ($user == $this->domainOwner) { continue; } $this->deleteTestUser($user->email); } if ($this->domainOwner) { $this->deleteTestUser($this->domainOwner->email); } if ($this->domainHosted) { $this->deleteTestDomain($this->domainHosted->namespace); } if ($this->publicDomainUser) { $this->deleteTestUser($this->publicDomainUser->email); } parent::tearDown(); } }