diff --git a/src/app/Utils.php b/src/app/Utils.php index df44ea73..29cb0283 100644 --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -1,621 +1,579 @@ country ? $net->country : $fallback; } /** * Return the country ISO code for the current request. */ public static function countryForRequest() { $request = \request(); $ip = $request->ip(); return self::countryForIP($ip); } /** * Return the number of days in the month prior to this one. * * @return int */ public static function daysInLastMonth() { $start = new Carbon('first day of last month'); $end = new Carbon('last day of last month'); return $start->diffInDays($end) + 1; } /** * Download a file from the interwebz and store it locally. * * @param string $source The source location * @param string $target The target location * @param bool $force Force the download (and overwrite target) * * @return void */ public static function downloadFile($source, $target, $force = false) { if (is_file($target) && !$force) { return; } \Log::info("Retrieving {$source}"); $fp = fopen($target, 'w'); $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $source); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_FILE, $fp); curl_exec($curl); if (curl_errno($curl)) { \Log::error("Request error on {$source}: " . curl_error($curl)); curl_close($curl); fclose($fp); unlink($target); return; } curl_close($curl); fclose($fp); } /** * Converts an email address to lower case. Keeps the LMTP shared folder * addresses character case intact. * * @param string $email Email address * * @return string Email address */ public static function emailToLower(string $email): string { // For LMTP shared folder address lower case the domain part only if (str_starts_with($email, 'shared+shared/')) { $pos = strrpos($email, '@'); $domain = substr($email, $pos + 1); $local = substr($email, 0, strlen($email) - strlen($domain) - 1); return $local . '@' . strtolower($domain); } return strtolower($email); } /** * Generate a passphrase. Not intended for use in production, so limited to environments that are not production. * * @return string */ public static function generatePassphrase() { if (\config('app.env') == 'production') { throw new \Exception("Thou shall not pass!"); } if (\config('app.passphrase')) { return \config('app.passphrase'); } $alphaLow = 'abcdefghijklmnopqrstuvwxyz'; $alphaUp = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $num = '0123456789'; $stdSpecial = '~`!@#$%^&*()-_+=[{]}\\|\'";:/?.>,<'; $source = $alphaLow . $alphaUp . $num . $stdSpecial; $result = ''; for ($x = 0; $x < 16; $x++) { $result .= substr($source, rand(0, (strlen($source) - 1)), 1); } return $result; } /** * Find an object that is the recipient for the specified address. * * @param string $address * * @return array */ public static function findObjectsByRecipientAddress($address) { $address = \App\Utils::normalizeAddress($address); list($local, $domainName) = explode('@', $address); $domain = \App\Domain::where('namespace', $domainName)->first(); if (!$domain) { return []; } $user = \App\User::where('email', $address)->first(); if ($user) { return [$user]; } $userAliases = \App\UserAlias::where('alias', $address)->get(); if (count($userAliases) > 0) { $users = []; foreach ($userAliases as $userAlias) { $users[] = $userAlias->user; } return $users; } $userAliases = \App\UserAlias::where('alias', "catchall@{$domain->namespace}")->get(); if (count($userAliases) > 0) { $users = []; foreach ($userAliases as $userAlias) { $users[] = $userAlias->user; } return $users; } return []; } /** * Retrieve the network ID and Type from a client address * * @param string $clientAddress The IPv4 or IPv6 address. * * @return array An array of ID and class or null and null. */ public static function getNetFromAddress($clientAddress) { if (strpos($clientAddress, ':') === false) { $net = \App\IP4Net::getNet($clientAddress); if ($net) { return [$net->id, \App\IP4Net::class]; } } else { $net = \App\IP6Net::getNet($clientAddress); if ($net) { return [$net->id, \App\IP6Net::class]; } } return [null, null]; } /** * Calculate the broadcast address provided a net number and a prefix. * * @param string $net A valid IPv6 network number. * @param int $prefix The network prefix. * * @return string */ public static function ip6Broadcast($net, $prefix) { $netHex = bin2hex(inet_pton($net)); // Overwriting first address string to make sure notation is optimal $net = inet_ntop(hex2bin($netHex)); // Calculate the number of 'flexible' bits $flexbits = 128 - $prefix; // Build the hexadecimal string of the last address $lastAddrHex = $netHex; // We start at the end of the string (which is always 32 characters long) $pos = 31; while ($flexbits > 0) { // Get the character at this position $orig = substr($lastAddrHex, $pos, 1); // Convert it to an integer $origval = hexdec($orig); // OR it with (2^flexbits)-1, with flexbits limited to 4 at a time $newval = $origval | (pow(2, min(4, $flexbits)) - 1); // Convert it back to a hexadecimal character $new = dechex($newval); // And put that character back in the string $lastAddrHex = substr_replace($lastAddrHex, $new, $pos, 1); // We processed one nibble, move to previous position $flexbits -= 4; $pos -= 1; } // Convert the hexadecimal string to a binary string $lastaddrbin = hex2bin($lastAddrHex); // And create an IPv6 address from the binary string $lastaddrstr = inet_ntop($lastaddrbin); return $lastaddrstr; } /** * Normalize an email address. * * This means to lowercase and strip components separated with recipient delimiters. * * @param ?string $address The address to normalize * @param bool $asArray Return an array with local and domain part * * @return string|array Normalized email address as string or array */ public static function normalizeAddress(?string $address, bool $asArray = false) { if ($address === null || $address === '') { return $asArray ? ['', ''] : ''; } $address = self::emailToLower($address); if (strpos($address, '@') === false) { return $asArray ? [$address, ''] : $address; } list($local, $domain) = explode('@', $address); if (strpos($local, '+') !== false) { $local = explode('+', $local)[0]; } return $asArray ? [$local, $domain] : "{$local}@{$domain}"; } - /** - * Provide all unique combinations of elements in $input, with order and duplicates irrelevant. - * - * @param array $input The input array of elements. - * - * @return array[] - */ - public static function powerSet(array $input): array - { - $output = []; - - for ($x = 0; $x < count($input); $x++) { - self::combine($input, $x + 1, 0, [], 0, $output); - } - - return $output; - } - /** * Returns the current user's email address or null. * * @return string */ public static function userEmailOrNull(): ?string { $user = Auth::user(); if (!$user) { return null; } return $user->email; } /** * Returns a random string consisting of a quantity of segments of a certain length joined. * * Example: * * ```php * $roomName = strtolower(\App\Utils::randStr(3, 3, '-'); * // $roomName == '3qb-7cs-cjj' * ``` * * @param int $length The length of each segment * @param int $qty The quantity of segments * @param string $join The string to use to join the segments * * @return string */ public static function randStr($length, $qty = 1, $join = '') { $chars = env('SHORTCODE_CHARS', self::CHARS); $randStrs = []; for ($x = 0; $x < $qty; $x++) { $randStrs[$x] = []; for ($y = 0; $y < $length; $y++) { $randStrs[$x][] = $chars[rand(0, strlen($chars) - 1)]; } shuffle($randStrs[$x]); $randStrs[$x] = implode('', $randStrs[$x]); } return implode($join, $randStrs); } /** * Returns a UUID in the form of an integer. * * @return int */ public static function uuidInt(): int { $hex = self::uuidStr(); $bin = pack('h*', str_replace('-', '', $hex)); $ids = unpack('L', $bin); $id = array_shift($ids); return $id; } /** * Returns a UUID in the form of a string. * * @return string */ public static function uuidStr(): string { return (string) Str::uuid(); } - private static function combine($input, $r, $index, $data, $i, &$output): void - { - $n = count($input); - - // Current cobination is ready - if ($index == $r) { - $output[] = array_slice($data, 0, $r); - return; - } - - // When no more elements are there to put in data[] - if ($i >= $n) { - return; - } - - // current is included, put next at next location - $data[$index] = $input[$i]; - self::combine($input, $r, $index + 1, $data, $i + 1, $output); - - // current is excluded, replace it with next (Note that i+1 - // is passed, but index is not changed) - self::combine($input, $r, $index, $data, $i + 1, $output); - } - /** * Create self URL * * @param string $route Route/Path/URL * @param int|null $tenantId Current tenant * * @todo Move this to App\Http\Controllers\Controller * * @return string Full URL */ public static function serviceUrl(string $route, $tenantId = null): string { if (preg_match('|^https?://|i', $route)) { return $route; } $url = \App\Tenant::getConfig($tenantId, 'app.public_url'); if (!$url) { $url = \App\Tenant::getConfig($tenantId, 'app.url'); } return rtrim(trim($url, '/') . '/' . ltrim($route, '/'), '/'); } /** * Create a configuration/environment data to be passed to * the UI * * @todo Move this to App\Http\Controllers\Controller * * @return array Configuration data */ public static function uiEnv(): array { $countries = include resource_path('countries.php'); $req_domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost()); $sys_domain = \config('app.domain'); $opts = [ 'app.name', 'app.url', 'app.domain', 'app.theme', 'app.webmail_url', 'app.support_email', 'app.company.copyright', 'app.companion_download_link', 'app.with_signup', 'mail.from.address' ]; $env = \app('config')->getMany($opts); $env['countries'] = $countries ?: []; $env['view'] = 'root'; $env['jsapp'] = 'user.js'; if ($req_domain == "admin.$sys_domain") { $env['jsapp'] = 'admin.js'; } elseif ($req_domain == "reseller.$sys_domain") { $env['jsapp'] = 'reseller.js'; } $env['paymentProvider'] = \config('services.payment_provider'); $env['stripePK'] = \config('services.stripe.public_key'); $env['languages'] = \App\Http\Controllers\ContentController::locales(); $env['menu'] = \App\Http\Controllers\ContentController::menu(); return $env; } /** * Set test exchange rates. * * @param array $rates: Exchange rates */ public static function setTestExchangeRates(array $rates): void { self::$testRates = $rates; } /** * Retrieve an exchange rate. * * @param string $sourceCurrency: Currency from which to convert * @param string $targetCurrency: Currency to convert to * * @return float Exchange rate */ public static function exchangeRate(string $sourceCurrency, string $targetCurrency): float { if (strcasecmp($sourceCurrency, $targetCurrency) == 0) { return 1.0; } if (isset(self::$testRates[$targetCurrency])) { return floatval(self::$testRates[$targetCurrency]); } $currencyFile = resource_path("exchangerates-$sourceCurrency.php"); //Attempt to find the reverse exchange rate, if we don't have the file for the source currency if (!file_exists($currencyFile)) { $rates = include resource_path("exchangerates-$targetCurrency.php"); if (!isset($rates[$sourceCurrency])) { throw new \Exception("Failed to find the reverse exchange rate for " . $sourceCurrency); } return 1.0 / floatval($rates[$sourceCurrency]); } $rates = include $currencyFile; if (!isset($rates[$targetCurrency])) { throw new \Exception("Failed to find exchange rate for " . $targetCurrency); } return floatval($rates[$targetCurrency]); } /** * A helper to display human-readable amount of money using * for specified currency and locale. * * @param int $amount Amount of money (in cents) * @param string $currency Currency code * @param string $locale Output locale * * @return string String representation, e.g. "9.99 CHF" */ public static function money(int $amount, $currency, $locale = 'de_DE'): string { $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); $result = $nf->formatCurrency(round($amount / 100, 2), $currency); // Replace non-breaking space return str_replace("\xC2\xA0", " ", $result); } /** * A helper to display human-readable percent value * for specified currency and locale. * * @param int|float $percent Percent value (0 to 100) * @param string $locale Output locale * * @return string String representation, e.g. "0 %", "7.7 %" */ public static function percent(int|float $percent, $locale = 'de_DE'): string { $nf = new \NumberFormatter($locale, \NumberFormatter::PERCENT); $sep = $nf->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); $result = sprintf('%.2F', $percent); $result = preg_replace('/\.00/', '', $result); $result = preg_replace('/(\.[0-9])0/', '\\1', $result); $result = str_replace('.', $sep, $result); return $result . ' %'; } } diff --git a/src/tests/Unit/DomainTest.php b/src/tests/Unit/DomainTest.php index 6e3a6ce3..439467af 100644 --- a/src/tests/Unit/DomainTest.php +++ b/src/tests/Unit/DomainTest.php @@ -1,138 +1,138 @@ 'test.com', 'status' => \array_sum($domainStatuses), 'type' => Domain::TYPE_EXTERNAL ] ); $domainStatuses = []; foreach ($statuses as $status) { if ($domain->status & $status) { $domainStatuses[] = $status; } } $this->assertSame($domain->status, \array_sum($domainStatuses)); // either one is true, but not both $this->assertSame( $domain->isNew() === in_array(Domain::STATUS_NEW, $domainStatuses), $domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domainStatuses) ); $this->assertTrue( $domain->isNew() === in_array(Domain::STATUS_NEW, $domainStatuses) ); $this->assertTrue( $domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domainStatuses) ); $this->assertTrue( $domain->isConfirmed() === in_array(Domain::STATUS_CONFIRMED, $domainStatuses) ); $this->assertTrue( $domain->isSuspended() === in_array(Domain::STATUS_SUSPENDED, $domainStatuses) ); $this->assertTrue( $domain->isDeleted() === in_array(Domain::STATUS_DELETED, $domainStatuses) ); $this->assertTrue( $domain->isLdapReady() === in_array(Domain::STATUS_LDAP_READY, $domainStatuses) ); $this->assertTrue( $domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domainStatuses) ); } } /** * Test basic Domain funtionality */ public function testDomainType(): void { $types = [ Domain::TYPE_PUBLIC, Domain::TYPE_HOSTED, Domain::TYPE_EXTERNAL, ]; - $domains = \App\Utils::powerSet($types); + $domains = \Tests\Utils::powerSet($types); foreach ($domains as $domain_types) { $domain = new Domain( [ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, 'type' => \array_sum($domain_types), ] ); $this->assertTrue($domain->isPublic() === in_array(Domain::TYPE_PUBLIC, $domain_types)); $this->assertTrue($domain->isHosted() === in_array(Domain::TYPE_HOSTED, $domain_types)); $this->assertTrue($domain->isExternal() === in_array(Domain::TYPE_EXTERNAL, $domain_types)); } } /** * Test domain hash generation */ public function testHash(): void { $domain = new Domain([ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, ]); $hash_code = $domain->hash(); $this->assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $hash_code); $hash_text = $domain->hash(Domain::HASH_TEXT); $this->assertMatchesRegularExpression('/^kolab-verify=[a-f0-9]{32}$/', $hash_text); $this->assertSame($hash_code, str_replace('kolab-verify=', '', $hash_text)); $hash_cname = $domain->hash(Domain::HASH_CNAME); $this->assertSame('kolab-verify', $hash_cname); $hash_code2 = $domain->hash(Domain::HASH_CODE); $this->assertSame($hash_code, $hash_code2); } } diff --git a/src/tests/Unit/SharedFolderTest.php b/src/tests/Unit/SharedFolderTest.php index beb5e479..ffac3527 100644 --- a/src/tests/Unit/SharedFolderTest.php +++ b/src/tests/Unit/SharedFolderTest.php @@ -1,85 +1,85 @@ 'test']); foreach ($folders as $folderStatuses) { $folder->status = \array_sum($folderStatuses); $folderStatuses = []; foreach ($statuses as $status) { if ($folder->status & $status) { $folderStatuses[] = $status; } } $this->assertSame($folder->status, \array_sum($folderStatuses)); // either one is true, but not both $this->assertSame( $folder->isNew() === in_array(SharedFolder::STATUS_NEW, $folderStatuses), $folder->isActive() === in_array(SharedFolder::STATUS_ACTIVE, $folderStatuses) ); $this->assertTrue( $folder->isNew() === in_array(SharedFolder::STATUS_NEW, $folderStatuses) ); $this->assertTrue( $folder->isActive() === in_array(SharedFolder::STATUS_ACTIVE, $folderStatuses) ); $this->assertTrue( $folder->isDeleted() === in_array(SharedFolder::STATUS_DELETED, $folderStatuses) ); $this->assertTrue( $folder->isLdapReady() === in_array(SharedFolder::STATUS_LDAP_READY, $folderStatuses) ); $this->assertTrue( $folder->isImapReady() === in_array(SharedFolder::STATUS_IMAP_READY, $folderStatuses) ); } $this->expectException(\Exception::class); $folder->status = 111; } /** * Test basic SharedFolder funtionality */ public function testSharedFolderType(): void { $folder = new SharedFolder(['name' => 'test']); foreach (SharedFolder::SUPPORTED_TYPES as $type) { $folder->type = $type; } $this->expectException(\Exception::class); $folder->type = 'unknown'; } } diff --git a/src/tests/Unit/UserTest.php b/src/tests/Unit/UserTest.php index b26bd2de..5e403357 100644 --- a/src/tests/Unit/UserTest.php +++ b/src/tests/Unit/UserTest.php @@ -1,119 +1,119 @@ 'user@email.com']); $user->password = 'test'; $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; $this->assertMatchesRegularExpression('/^\$2y\$04\$[0-9a-zA-Z\/.]{53}$/', $user->password); $this->assertSame($ssh512, $user->password_ldap); } /** * Test User password mutator */ public function testSetPasswordLdapAttribute(): void { $user = new User(['email' => 'user@email.com']); $user->password_ldap = 'test'; $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; $this->assertMatchesRegularExpression('/^\$2y\$04\$[0-9a-zA-Z\/.]{53}$/', $user->password); $this->assertSame($ssh512, $user->password_ldap); } /** * Test User password validation */ public function testPasswordValidation(): void { $user = new User(['email' => 'user@email.com']); $user->password = 'test'; $this->assertSame(true, $user->validateCredentials('user@email.com', 'test')); $this->assertSame(false, $user->validateCredentials('user@email.com', 'wrong')); $this->assertSame(true, $user->validateCredentials('User@Email.Com', 'test')); $this->assertSame(false, $user->validateCredentials('wrong', 'test')); // Ensure the fallback to the ldap_password works if the current password is empty $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; $ldapUser = new User(['email' => 'user2@email.com']); $ldapUser->setRawAttributes(['password' => '', 'password_ldap' => $ssh512, 'email' => 'user2@email.com']); $this->assertSame($ldapUser->password, ''); $this->assertSame($ldapUser->password_ldap, $ssh512); $this->assertSame(true, $ldapUser->validateCredentials('user2@email.com', 'test', false)); $ldapUser->delete(); } /** * Test basic User funtionality */ public function testStatus(): void { $statuses = [ User::STATUS_NEW, User::STATUS_ACTIVE, User::STATUS_SUSPENDED, User::STATUS_DELETED, User::STATUS_IMAP_READY, User::STATUS_LDAP_READY, User::STATUS_DEGRADED, User::STATUS_RESTRICTED, ]; - $users = \App\Utils::powerSet($statuses); + $users = \Tests\Utils::powerSet($statuses); foreach ($users as $user_statuses) { $user = new User( [ 'email' => 'user@email.com', 'status' => \array_sum($user_statuses), ] ); $this->assertTrue($user->isNew() === in_array(User::STATUS_NEW, $user_statuses)); $this->assertTrue($user->isActive() === in_array(User::STATUS_ACTIVE, $user_statuses)); $this->assertTrue($user->isSuspended() === in_array(User::STATUS_SUSPENDED, $user_statuses)); $this->assertTrue($user->isDeleted() === in_array(User::STATUS_DELETED, $user_statuses)); $this->assertTrue($user->isLdapReady() === in_array(User::STATUS_LDAP_READY, $user_statuses)); $this->assertTrue($user->isImapReady() === in_array(User::STATUS_IMAP_READY, $user_statuses)); $this->assertTrue($user->isDegraded() === in_array(User::STATUS_DEGRADED, $user_statuses)); $this->assertTrue($user->isRestricted() === in_array(User::STATUS_RESTRICTED, $user_statuses)); } } /** * Test setStatusAttribute exception */ public function testStatusInvalid(): void { $this->expectException(\Exception::class); $user = new User( [ 'email' => 'user@email.com', 'status' => 1234567, ] ); } } diff --git a/src/tests/Unit/UtilsTest.php b/src/tests/Unit/UtilsTest.php index c6838e95..bb164c01 100644 --- a/src/tests/Unit/UtilsTest.php +++ b/src/tests/Unit/UtilsTest.php @@ -1,216 +1,216 @@ delete(); \App\IP6Net::where('net_number', inet_pton('2001:db8::ff00:42:0'))->delete(); $this->assertSame('', Utils::countryForIP('127.0.0.1', '')); $this->assertSame('CH', Utils::countryForIP('127.0.0.1')); $this->assertSame('', Utils::countryForIP('2001:db8::ff00:42:1', '')); $this->assertSame('CH', Utils::countryForIP('2001:db8::ff00:42:1')); \App\IP4Net::create([ 'net_number' => '127.0.0.0', 'net_broadcast' => '127.255.255.255', 'net_mask' => 8, 'country' => 'US', 'rir_name' => 'test', 'serial' => 1, ]); \App\IP6Net::create([ 'net_number' => '2001:db8::ff00:42:0', 'net_broadcast' => \App\Utils::ip6Broadcast('2001:db8::ff00:42:0', 8), 'net_mask' => 8, 'country' => 'PL', 'rir_name' => 'test', 'serial' => 1, ]); $this->assertSame('US', Utils::countryForIP('127.0.0.1', '')); $this->assertSame('US', Utils::countryForIP('127.0.0.1')); $this->assertSame('PL', Utils::countryForIP('2001:db8::ff00:42:1', '')); $this->assertSame('PL', Utils::countryForIP('2001:db8::ff00:42:1')); \App\IP4Net::where('net_number', inet_pton('127.0.0.0'))->delete(); \App\IP6Net::where('net_number', inet_pton('2001:db8::ff00:42:0'))->delete(); } /** * Test for Utils::emailToLower() */ public function testEmailToLower(): void { $this->assertSame('test@test.tld', Utils::emailToLower('test@Test.Tld')); $this->assertSame('test@test.tld', Utils::emailToLower('Test@Test.Tld')); $this->assertSame('shared+shared/Test@test.tld', Utils::emailToLower('shared+shared/Test@Test.Tld')); } /** * Test for Utils::money() */ public function testMoney(): void { $this->assertSame('-0,01 CHF', Utils::money(-1, 'CHF')); $this->assertSame('0,00 CHF', Utils::money(0, 'CHF')); $this->assertSame('1,11 €', Utils::money(111, 'EUR')); $this->assertSame('1,00 CHF', Utils::money(100, 'CHF')); $this->assertSame('€0.00', Utils::money(0, 'EUR', 'en_US')); } /** * Test for Utils::percent() */ public function testPercent(): void { $this->assertSame('0 %', Utils::percent(0)); $this->assertSame('10 %', Utils::percent(10.0)); $this->assertSame('10,12 %', Utils::percent(10.12)); } /** * Test for Utils::normalizeAddress() */ public function testNormalizeAddress(): void { $this->assertSame('', Utils::normalizeAddress('')); $this->assertSame('', Utils::normalizeAddress(null)); $this->assertSame('test', Utils::normalizeAddress('TEST')); $this->assertSame('test@domain.tld', Utils::normalizeAddress('Test@Domain.TLD')); $this->assertSame('test@domain.tld', Utils::normalizeAddress('Test+Trash@Domain.TLD')); $this->assertSame(['', ''], Utils::normalizeAddress('', true)); $this->assertSame(['', ''], Utils::normalizeAddress(null, true)); $this->assertSame(['test', ''], Utils::normalizeAddress('TEST', true)); $this->assertSame(['test', 'domain.tld'], Utils::normalizeAddress('Test@Domain.TLD', true)); $this->assertSame(['test', 'domain.tld'], Utils::normalizeAddress('Test+Trash@Domain.TLD', true)); } /** - * Test for Utils::powerSet() + * Test for Tests\Utils::powerSet() */ public function testPowerSet(): void { $set = []; - $result = Utils::powerSet($set); + $result = \Tests\Utils::powerSet($set); $this->assertIsArray($result); $this->assertCount(0, $result); $set = ["a1"]; - $result = Utils::powerSet($set); + $result = \Tests\Utils::powerSet($set); $this->assertIsArray($result); $this->assertCount(1, $result); $this->assertTrue(in_array(["a1"], $result)); $set = ["a1", "a2"]; - $result = Utils::powerSet($set); + $result = \Tests\Utils::powerSet($set); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertTrue(in_array(["a1"], $result)); $this->assertTrue(in_array(["a2"], $result)); $this->assertTrue(in_array(["a1", "a2"], $result)); $set = ["a1", "a2", "a3"]; - $result = Utils::powerSet($set); + $result = \Tests\Utils::powerSet($set); $this->assertIsArray($result); $this->assertCount(7, $result); $this->assertTrue(in_array(["a1"], $result)); $this->assertTrue(in_array(["a2"], $result)); $this->assertTrue(in_array(["a3"], $result)); $this->assertTrue(in_array(["a1", "a2"], $result)); $this->assertTrue(in_array(["a1", "a3"], $result)); $this->assertTrue(in_array(["a2", "a3"], $result)); $this->assertTrue(in_array(["a1", "a2", "a3"], $result)); } /** * Test for Utils::serviceUrl() */ public function testServiceUrl(): void { $public_href = 'https://public.url/cockpit'; $local_href = 'https://local.url/cockpit'; \config([ 'app.url' => $local_href, 'app.public_url' => '', ]); $this->assertSame($local_href, Utils::serviceUrl('')); $this->assertSame($local_href . '/unknown', Utils::serviceUrl('unknown')); $this->assertSame($local_href . '/unknown', Utils::serviceUrl('/unknown')); \config([ 'app.url' => $local_href, 'app.public_url' => $public_href, ]); $this->assertSame($public_href, Utils::serviceUrl('')); $this->assertSame($public_href . '/unknown', Utils::serviceUrl('unknown')); $this->assertSame($public_href . '/unknown', Utils::serviceUrl('/unknown')); } /** * Test for Utils::uuidInt() */ public function testUuidInt(): void { $result = Utils::uuidInt(); $this->assertTrue(is_int($result)); $this->assertTrue($result > 0); } /** * Test for Utils::uuidStr() */ public function testUuidStr(): void { $result = Utils::uuidStr(); $this->assertTrue(is_string($result)); $this->assertTrue(strlen($result) === 36); $this->assertTrue(preg_match('/[^a-f0-9-]/i', $result) === 0); } /** * Test for Utils::exchangeRate() */ public function testExchangeRate(): void { $this->assertSame(1.0, Utils::exchangeRate("DUMMY", "dummy")); // Exchange rates are volatile, can't test with high accuracy. $this->assertTrue(Utils::exchangeRate("CHF", "EUR") >= 0.88); //$this->assertEqualsWithDelta(0.90503424978382, Utils::exchangeRate("CHF", "EUR"), PHP_FLOAT_EPSILON); $this->assertTrue(Utils::exchangeRate("EUR", "CHF") <= 1.12); //$this->assertEqualsWithDelta(1.1049305595217682, Utils::exchangeRate("EUR", "CHF"), PHP_FLOAT_EPSILON); $this->expectException(\Exception::class); $this->assertSame(1.0, Utils::exchangeRate("CHF", "FOO")); $this->expectException(\Exception::class); $this->assertSame(1.0, Utils::exchangeRate("FOO", "CHF")); } } diff --git a/src/tests/Utils.php b/src/tests/Utils.php new file mode 100644 index 00000000..ead728e1 --- /dev/null +++ b/src/tests/Utils.php @@ -0,0 +1,53 @@ += $n) { + return; + } + + // current is included, put next at next location + $data[$index] = $input[$i]; + self::combine($input, $r, $index + 1, $data, $i + 1, $output); + + // current is excluded, replace it with next (Note that i+1 + // is passed, but index is not changed) + self::combine($input, $r, $index, $data, $i + 1, $output); + } +}