diff --git a/src/.env.example b/src/.env.example index f12f82da..a744a143 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,82 +1,87 @@ APP_NAME=Kolab APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://127.0.0.1:8000 APP_DOMAIN=kolabnow.com LOG_CHANNEL=stack DB_CONNECTION=mysql DB_DATABASE=kolabdev DB_HOST=127.0.0.1 DB_PASSWORD=kolab DB_PORT=3306 DB_USERNAME=kolabdev BROADCAST_DRIVER=log CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=file SESSION_LIFETIME=120 IMAP_URI=ssl://127.0.0.1:993 IMAP_ADMIN_LOGIN=cyrus-admin IMAP_ADMIN_PASSWORD=Welcome2KolabSystems IMAP_VERIFY_PEER=true IMAP_VERIFY_NAME=true IMAP_CAFILE=null LDAP_BASE_DN="dc=mgmt,dc=com" LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com" LDAP_HOSTS=127.0.0.1 LDAP_PORT=389 LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_SERVICE_BIND_PW="Welcome2KolabSystems" LDAP_USE_SSL=false LDAP_USE_TLS=false # Administrative LDAP_ADMIN_BIND_DN="cn=Directory Manager" LDAP_ADMIN_BIND_PW="Welcome2KolabSystems" LDAP_ADMIN_ROOT_DN="dc=mgmt,dc=com" # Hosted (public registration) LDAP_HOSTED_BIND_DN="uid=hosted-kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_HOSTED_BIND_PW="Welcome2KolabSystems" LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com" REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 SWOOLE_HTTP_HOST=127.0.0.1 SWOOLE_HTTP_PORT=8000 MAIL_DRIVER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="noreply@example.com" MAIL_FROM_NAME="Example.com" MAIL_REPLYTO_ADDRESS=null MAIL_REPLYTO_NAME=null +DNS_TTL=3600 +DNS_SPF="v=spf1 mx -all" +DNS_STATIC="%s. MX 10 ext-mx01.mykolab.com." +DNS_COPY_FROM=null + AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" JWT_SECRET= diff --git a/src/app/Domain.php b/src/app/Domain.php index 456ae8f6..0e50f210 100644 --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -1,290 +1,303 @@ morphOne('App\Entitlement', 'entitleable'); } /** * Return list of public+active domain names */ public static function getPublicDomains(): array { $where = sprintf('(type & %s) AND (status & %s)', Domain::TYPE_PUBLIC, Domain::STATUS_ACTIVE); return self::whereRaw($where)->get(['namespace'])->map(function ($domain) { return $domain->namespace; })->toArray(); } /** * Returns whether this domain is active. * * @return bool */ public function isActive(): bool { return $this->status & self::STATUS_ACTIVE; } /** * Returns whether this domain is confirmed the ownership of. * * @return bool */ public function isConfirmed(): bool { return $this->status & self::STATUS_CONFIRMED; } /** * Returns whether this domain is deleted. * * @return bool */ public function isDeleted(): bool { return $this->status & self::STATUS_DELETED; } /** * Returns whether this domain is registered with us. * * @return bool */ public function isExternal(): bool { return $this->type & self::TYPE_EXTERNAL; } /** * Returns whether this domain is hosted with us. * * @return bool */ public function isHosted(): bool { return $this->type & self::TYPE_HOSTED; } /** * Returns whether this domain is new. * * @return bool */ public function isNew(): bool { return $this->status & self::STATUS_NEW; } /** * Returns whether this domain is public. * * @return bool */ public function isPublic(): bool { return $this->type & self::TYPE_PUBLIC; } /** * Returns whether this domain is registered in LDAP. * * @return bool */ public function isLdapReady(): bool { return $this->status & self::STATUS_LDAP_READY; } /** * Returns whether this domain is suspended. * * @return bool */ public function isSuspended(): bool { return $this->status & self::STATUS_SUSPENDED; } /** * Returns whether this (external) domain has been verified * to exist in DNS. * * @return bool */ +/* public function isVerified(): bool { return $this->status & self::STATUS_VERIFIED; } - +*/ /** * Domain status mutator * * @throws \Exception */ public function setStatusAttribute($status) { $new_status = 0; $allowed_values = [ self::STATUS_NEW, self::STATUS_ACTIVE, self::STATUS_CONFIRMED, self::STATUS_SUSPENDED, self::STATUS_DELETED, self::STATUS_LDAP_READY, - self::STATUS_VERIFIED, +// self::STATUS_VERIFIED, ]; foreach ($allowed_values as $value) { if ($status & $value) { $new_status |= $value; $status ^= $value; } } if ($status > 0) { throw new \Exception("Invalid domain status: {$status}"); } $this->attributes['status'] = $new_status; } /** * Ownership verification by checking for a TXT (or CNAME) record * in the domain's DNS (that matches the verification hash). * * @return bool True if verification was successful, false otherwise * @throws \Exception Throws exception on DNS or DB errors */ public function confirm(): bool { if ($this->isConfirmed()) { return true; } - $hash = $this->hash(); + $hash = $this->hash(self::HASH_TEXT); $confirmed = false; // Get DNS records and find a matching TXT entry $records = \dns_get_record($this->namespace, DNS_TXT); if ($records === false) { - throw new \Exception("Failed to get DNS record for $domain"); + throw new \Exception("Failed to get DNS record for {$this->namespace}"); } foreach ($records as $record) { if ($record['txt'] === $hash) { $confirmed = true; break; } } // Get DNS records and find a matching CNAME entry // Note: some servers resolve every non-existing name // so we need to define left and right side of the CNAME record // i.e.: kolab-verify IN CNAME .domain.tld. if (!$confirmed) { - $cname = $this->hash(true) . '.' . $this->namespace; + $cname = $this->hash(self::HASH_CODE) . '.' . $this->namespace; $records = \dns_get_record('kolab-verify.' . $this->namespace, DNS_CNAME); if ($records === false) { throw new \Exception("Failed to get DNS record for $domain"); } foreach ($records as $records) { if ($records['target'] === $cname) { $confirmed = true; break; } } } if ($confirmed) { $this->status |= Domain::STATUS_CONFIRMED; $this->save(); } return $confirmed; } /** * Generate a verification hash for this domain * - * @param bool $short Return short version (with kolab-verify= prefix) + * @param int $mod One of: HASH_CNAME, HASH_CODE (Default), HASH_TEXT * * @return string Verification hash */ - public function hash($short = false): string + public function hash($mod = null): string { - $hash = \md5('hkccp-verify-' . $this->namespace . $this->id); + $cname = 'kolab-verify'; + + if ($mod === self::HASH_CNAME) { + return $cname; + } + + $hash = \md5('hkccp-verify-' . $this->namespace); - return $short ? $hash : "kolab-verify=$hash"; + return $mod === self::HASH_TEXT ? "$cname=$hash" : $hash; } /** * Verify if a domain exists in DNS * * @return bool True if registered, False otherwise * @throws \Exception Throws exception on DNS or DB errors */ +/* public function verify(): bool { if ($this->isVerified()) { return true; } $record = \dns_get_record($this->namespace, DNS_SOA); if ($record === false) { throw new \Exception("Failed to get DNS record for {$this->namespace}"); } if (!empty($record)) { $this->status |= Domain::STATUS_VERIFIED; $this->save(); return true; } return false; } +*/ } diff --git a/src/app/Http/Controllers/API/DomainsController.php b/src/app/Http/Controllers/API/DomainsController.php new file mode 100644 index 00000000..432ef021 --- /dev/null +++ b/src/app/Http/Controllers/API/DomainsController.php @@ -0,0 +1,213 @@ +confirm()) { + return response()->json(['status' => 'error']); + } + + return response()->json(['status' => 'success']); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * + * @return \Illuminate\Http\Response + */ + public function destroy($id) + { + // + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * + * @return \Illuminate\Http\Response + */ + public function edit($id) + { + // + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + // + } + + /** + * Get the information about the specified domain. + * + * @param int $id Domain identifier + * + * @return \Illuminate\Http\Response + */ + public function show($id) + { + $domain = Domain::findOrFail($id); + + // Only owner (or admin) has access to the domain + if (!self::hasAccess($domain)) { + return abort(403); + } + + $response = $domain->toArray(); + + // Add hash information to the response + $response['hash_text'] = $domain->hash(Domain::HASH_TEXT); + $response['hash_cname'] = $domain->hash(Domain::HASH_CNAME); + $response['hash_code'] = $domain->hash(Domain::HASH_CODE); + + // Add DNS/MX configuration for the domain + $response['dns'] = self::getDNSConfig($domain); + $response['config'] = self::getMXConfig($domain->namespace); + + $response['confirmed'] = $domain->isConfirmed(); + + return response()->json($response); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * + * @return \Illuminate\Http\Response + */ + public function update(Request $request, $id) + { + // + } + + /** + * Provide DNS MX information to configure specified domain for + */ + protected static function getMXConfig(string $namespace): array + { + $entries = []; + + // copy MX entries from an existing domain + if ($master = \config('dns.copyfrom')) { + // TODO: cache this lookup + foreach ((array) dns_get_record($master, DNS_MX) as $entry) { + $entries[] = sprintf( + "@\t%s\t%s\tMX\t%d %s.", + \config('dns.ttl', $entry['ttl']), + $entry['class'], + $entry['pri'], + $entry['target'] + ); + } + } elseif ($static = \config('dns.static')) { + $entries[] = strtr($static, array('\n' => "\n", '%s' => $namespace)); + } + + // display SPF settings + if ($spf = \config('dns.spf')) { + $entries[] = ';'; + foreach (['TXT', 'SPF'] as $type) { + $entries[] = sprintf( + "@\t%s\tIN\t%s\t\"%s\"", + \config('dns.ttl'), + $type, + $spf + ); + } + } + + return $entries; + } + + /** + * Provide sample DNS config for domain confirmation + */ + protected static function getDNSConfig(Domain $domain): array + { + $serial = date('Ymd01'); + $hash_txt = $domain->hash(Domain::HASH_TEXT); + $hash_cname = $domain->hash(Domain::HASH_CNAME); + $hash = $domain->hash(Domain::HASH_CODE); + + return [ + "@ IN SOA ns1.dnsservice.com. hostmaster.{$domain->namespace}. (", + " {$serial} 10800 3600 604800 86400 )", + ";", + "@ IN A ", + "www IN A ", + ";", + "{$hash_cname}.{$domain->namespace}. IN CNAME {$hash}.{$domain->namespace}.", + "@ 3600 TXT \"{$hash_txt}\"", + ]; + } + + /** + * Check if the current user has access to the domain + * + * @param \App\Domain Domain + * + * @return bool True if current user has access, False otherwise + */ + protected static function hasAccess(Domain $domain): bool + { + $user = Auth::guard()->user(); + $entitlement = $domain->entitlement()->first(); + + // TODO: Admins + + return $entitlement && $entitlement->owner_id == $user->id; + } +} diff --git a/src/app/Http/Controllers/API/UsersController.php b/src/app/Http/Controllers/API/UsersController.php index b88226b6..f845897c 100644 --- a/src/app/Http/Controllers/API/UsersController.php +++ b/src/app/Http/Controllers/API/UsersController.php @@ -1,247 +1,247 @@ middleware('auth:api', ['except' => ['login']]); } /** * Helper method for other controllers with user auto-logon * functionality * * @param \App\User $user User model object */ public static function logonResponse(User $user) { $token = auth()->login($user); return response()->json([ 'status' => 'success', 'access_token' => $token, 'token_type' => 'bearer', 'expires_in' => Auth::guard()->factory()->getTTL() * 60, ]); } /** * Display a listing of the resources. * * The user themself, and other user entitlements. * * @return \Illuminate\Http\Response */ public function index() { $user = Auth::user(); if (!$user) { return response()->json(['error' => 'unauthorized'], 401); } $result = [$user]; $user->entitlements()->each( function ($entitlement) { $result[] = User::find($entitlement->user_id); } ); return response()->json($result); } /** * Get the authenticated User * * @return \Illuminate\Http\JsonResponse */ public function info() { $user = $this->guard()->user(); $response = $user->toArray(); $response['statusInfo'] = self::statusInfo($user); return response()->json($response); } /** * Get a JWT token via given credentials. * * @param \Illuminate\Http\Request $request The API request. * * @return \Illuminate\Http\JsonResponse */ public function login(Request $request) { $credentials = $request->only('email', 'password'); if ($token = $this->guard()->attempt($credentials)) { return $this->respondWithToken($token); } return response()->json(['error' => 'Unauthorized'], 401); } /** * Log the user out (Invalidate the token) * * @return \Illuminate\Http\JsonResponse */ public function logout() { $this->guard()->logout(); return response()->json(['message' => 'Successfully logged out']); } /** * Refresh a token. * * @return \Illuminate\Http\JsonResponse */ public function refresh() { return $this->respondWithToken($this->guard()->refresh()); } /** * Get the token array structure. * * @param string $token Respond with this token. * * @return \Illuminate\Http\JsonResponse */ protected function respondWithToken($token) { return response()->json( [ 'access_token' => $token, 'token_type' => 'bearer', 'expires_in' => $this->guard()->factory()->getTTL() * 60 ] ); } /** * Display the specified resource. * * @param int $id The account to show information for. * * @return \Illuminate\Http\Response */ public function show($id) { $user = Auth::user(); if (!$user) { return abort(403); } $result = false; $user->entitlements()->each( function ($entitlement) { if ($entitlement->user_id == $id) { $result = true; } } ); if ($user->id == $id) { $result = true; } if (!$result) { return abort(404); } return \App\User::find($id); } /** * User status (extended) information * * @param \App\User $user User object * * @return array Status information */ public static function statusInfo(User $user): array { $status = 'new'; $process = []; $steps = [ 'user-new' => true, 'user-ldap-ready' => 'isLdapReady', 'user-imap-ready' => 'isImapReady', ]; if ($user->isDeleted()) { $status = 'deleted'; } elseif ($user->isSuspended()) { $status = 'suspended'; } elseif ($user->isActive()) { $status = 'active'; } list ($local, $domain) = explode('@', $user->email); $domain = Domain::where('namespace', $domain)->first(); // If that is not a public domain, add domain specific steps if (!$domain->isPublic()) { $steps['domain-new'] = true; $steps['domain-ldap-ready'] = 'isLdapReady'; - $steps['domain-verified'] = 'isVerified'; +// $steps['domain-verified'] = 'isVerified'; $steps['domain-confirmed'] = 'isConfirmed'; } // Create a process check list foreach ($steps as $step_name => $func) { $object = strpos($step_name, 'user-') === 0 ? $user : $domain; $step = [ 'label' => $step_name, 'title' => __("app.process-{$step_name}"), - 'state' => is_bool($func) ? $func : $object->{$func}(), + 'state' => false,//is_bool($func) ? $func : $object->{$func}(), ]; if ($step_name == 'domain-confirmed' && !$step['state']) { $step['link'] = "/domain/{$domain->id}"; } $process[] = $step; } return [ 'process' => $process, 'status' => $status, ]; } /** * Get the guard to be used during authentication. * * @return \Illuminate\Contracts\Auth\Guard */ public function guard() { return Auth::guard(); } } diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php index 34670685..4baf2f54 100644 --- a/src/app/Observers/DomainObserver.php +++ b/src/app/Observers/DomainObserver.php @@ -1,93 +1,96 @@ {$domain->getKeyName()} = $allegedly_unique; break; } } $domain->status |= Domain::STATUS_NEW; } /** * Handle the domain "created" event. * * @param \App\Domain $domain The domain. * * @return void */ public function created(Domain $domain) { // Create domain record in LDAP, then check if it exists in DNS +/* $chain = [ new \App\Jobs\ProcessDomainVerify($domain), ]; \App\Jobs\ProcessDomainCreate::withChain($chain)->dispatch($domain); +*/ + \App\Jobs\ProcessDomainCreate::dispatch($domain); } /** * Handle the domain "updated" event. * * @param \App\Domain $domain The domain. * * @return void */ public function updated(Domain $domain) { // } /** * Handle the domain "deleted" event. * * @param \App\Domain $domain The domain. * * @return void */ public function deleted(Domain $domain) { // } /** * Handle the domain "restored" event. * * @param \App\Domain $domain The domain. * * @return void */ public function restored(Domain $domain) { // } /** * Handle the domain "force deleted" event. * * @param \App\Domain $domain The domain. * * @return void */ public function forceDeleted(Domain $domain) { // } } diff --git a/src/app/Sku.php b/src/app/Sku.php index a99718c7..9fdedef9 100644 --- a/src/app/Sku.php +++ b/src/app/Sku.php @@ -1,85 +1,90 @@ 'integer' ]; protected $fillable = [ 'title', 'description', 'cost', 'units_free', 'period', 'handler_class', 'active' ]; /** * List the entitlements that consume this SKU. * * @return Entitlement[] */ public function entitlements() { return $this->hasMany('App\Entitlement'); } public function packages() { return $this->belongsToMany( 'App\Package', 'package_skus' )->using('App\PackageSku')->withPivot(['qty']); } /** * Register (default) SKU entitlement for specified user. * This method should be used e.g. on user creation when we have * a set of SKUs and want to create entitlements for them (using * default values). */ public function registerEntitlement(\App\User $user, array $params = []) { + if (!$this->active) { + \Log::debug("Skipped registration of an entitlement for non-active SKU ($this->title)"); + return; + } + $wallet = $user->wallets()->get()[0]; $entitlement = new \App\Entitlement(); $entitlement->owner_id = $user->id; $entitlement->wallet_id = $wallet->id; $entitlement->sku_id = $this->id; $entitlement->entitleable_type = $this->handler_class::entitleableClass(); if ($user instanceof $entitlement->entitleable_type) { $entitlement->entitleable_id = $user->id; } else { foreach ($params as $param) { if ($param instanceof $entitlement->entitleable_type) { $entitlement->entitleable_id = $param->id; break; } } } if (empty($entitlement->entitleable_id)) { if (method_exists($this->handler_class, 'createDefaultEntitleable')) { $entitlement->entitleable_id = $this->handler_class::createDefaultEntitleable($user); } else { - // error + throw new Exception("Failed to create an entitlement for SKU ($this->title). Missing entitleable_id."); } } $entitlement->save(); } } diff --git a/src/config/dns.php b/src/config/dns.php new file mode 100644 index 00000000..a0ae9c51 --- /dev/null +++ b/src/config/dns.php @@ -0,0 +1,8 @@ + env('DNS_TTL', 3600), + 'spf' => env('DNS_SPF', null), + 'static' => env('DNS_STATIC', null), + 'copyfrom' => env('DNS_COPY_FROM', null), +]; diff --git a/src/database/seeds/SkuSeeder.php b/src/database/seeds/SkuSeeder.php index b4ebaa6e..c1c73a0e 100644 --- a/src/database/seeds/SkuSeeder.php +++ b/src/database/seeds/SkuSeeder.php @@ -1,118 +1,118 @@ 'mailbox', 'description' => 'Just a mailbox', 'cost' => 444, 'units_free' => 0, 'period' => 'monthly', 'handler_class' => 'App\Handlers\Mailbox', 'active' => true, ] ); Sku::create( [ 'title' => 'domain', 'description' => 'Somewhere to place a mailbox', 'cost' => 100, 'period' => 'monthly', 'handler_class' => 'App\Handlers\Domain', 'active' => true, ] ); Sku::create( [ 'title' => 'domain-registration', 'description' => 'Register a domain with us', 'cost' => 101, 'period' => 'yearly', 'handler_class' => 'App\Handlers\DomainRegistration', 'active' => false, ] ); Sku::create( [ 'title' => 'domain-hosting', 'description' => 'Host a domain that is externally registered', 'cost' => 100, 'units_free' => 1, 'period' => 'monthly', 'handler_class' => 'App\Handlers\DomainHosting', - 'active' => false, + 'active' => true, ] ); Sku::create( [ 'title' => 'domain-relay', 'description' => 'A domain you host at home, for which we relay email', 'cost' => 103, 'period' => 'monthly', 'handler_class' => 'App\Handlers\DomainRelay', 'active' => false, ] ); Sku::create( [ 'title' => 'storage', 'description' => 'Some wiggle room', 'cost' => 25, 'units_free' => 2, 'period' => 'monthly', 'handler_class' => 'App\Handlers\Storage', 'active' => true, ] ); Sku::create( [ 'title' => 'groupware', 'description' => 'groupware functions', 'cost' => 555, 'units_free' => 0, 'period' => 'monthly', 'handler_class' => 'App\Handlers\Groupware', 'active' => true, ] ); Sku::create( [ 'title' => 'resource', 'description' => 'Reservation taker', 'cost' => 101, 'period' => 'monthly', 'handler_class' => 'App\Handlers\Resource', 'active' => false, ] ); Sku::create( [ 'title' => 'shared_folder', 'description' => 'A shared folder', 'cost' => 89, 'period' => 'monthly', 'handler_class' => 'App\Handlers\SharedFolder', 'active' => false, ] ); } } diff --git a/src/phpunit.xml b/src/phpunit.xml index 447c94f0..2eccde90 100644 --- a/src/phpunit.xml +++ b/src/phpunit.xml @@ -1,34 +1,35 @@ - ./tests/Unit + tests/Unit - ./tests/Feature + tests/Feature + tests/Feature/Jobs/ProcessDomainVerifyTest.php ./app diff --git a/src/resources/js/app.js b/src/resources/js/app.js index dd4a4c83..9cec1bac 100644 --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -1,118 +1,123 @@ /** * First we will load all of this project's JavaScript dependencies which * includes Vue and other libraries. It is a great starting point when * building robust, powerful web applications using Vue and Laravel. */ require('./bootstrap') window.Vue = require('vue') import AppComponent from '../vue/components/App' import MenuComponent from '../vue/components/Menu' import router from '../vue/js/routes.js' import store from '../vue/js/store' import VueToastr from '@deveodk/vue-toastr' // Add a response interceptor for general/validation error handler // This have to be before Vue and Router setup. Otherwise we would // not be able to handle axios responses initiated from inside // components created/mounted handlers (e.g. signup code verification link) window.axios.interceptors.response.use( response => { // Do nothing return response }, error => { var error_msg if (error.response && error.response.status == 422) { error_msg = "Form validation error" $.each(error.response.data.errors || {}, (idx, msg) => { $('form').each((i, form) => { const input_name = ($(form).data('validation-prefix') || '') + idx const input = $('#' + input_name) if (input.length) { input.addClass('is-invalid') .parent().append($('
') .text($.type(msg) === 'string' ? msg : msg.join('
'))) return false } }); }) $('form .is-invalid').first().focus() } else if (error.response && error.response.data) { error_msg = error.response.data.message } else { error_msg = error.request ? error.request.statusText : error.message } app.$toastr('error', error_msg || "Server Error", 'Error') // Pass the error as-is return Promise.reject(error) } ) const app = new Vue({ el: '#app', components: { 'app-component': AppComponent, 'menu-component': MenuComponent }, store, router, data() { return { isLoading: true } }, mounted() { this.$root.$on('clearFormValidation', (form) => { this.clearFormValidation(form) }) }, methods: { // Clear (bootstrap) form validation state clearFormValidation(form) { $(form).find('.is-invalid').removeClass('is-invalid') $(form).find('.invalid-feedback').remove() }, // Set user state to "logged in" - loginUser(token) { + loginUser(token, dashboard) { store.commit('loginUser') localStorage.setItem('token', token) axios.defaults.headers.common.Authorization = 'Bearer ' + token - router.push({ name: 'dashboard' }) + + if (dashboard !== false) { + router.push(store.state.afterLogin || { name: 'dashboard' }) + } + + store.state.afterLogin = null }, // Set user state to "not logged in" logoutUser() { store.commit('logoutUser') localStorage.setItem('token', '') delete axios.defaults.headers.common.Authorization router.push({ name: 'login' }) }, // Display "loading" overlay (to be used by route components) startLoading() { this.isLoading = true // Lock the UI with the 'loading...' element $('#app').append($('
Loading
')) }, // Hide "loading" overlay stopLoading() { $('#app > .app-loader').fadeOut() this.isLoading = false } } }) Vue.use(VueToastr, { defaultPosition: 'toast-bottom-right', defaultTimeout: 50000 }) diff --git a/src/resources/sass/_variables.scss b/src/resources/sass/_variables.scss index 63954cbf..35dc03c3 100644 --- a/src/resources/sass/_variables.scss +++ b/src/resources/sass/_variables.scss @@ -1,19 +1,22 @@ // Body $body-bg: #fff; // Typography $font-family-sans-serif: 'Nunito', sans-serif; $font-size-base: 0.9rem; $line-height-base: 1.6; // Colors $blue: #3490dc; $indigo: #6574cd; $purple: #9561e2; $pink: #f66d9b; $red: #e3342f; $orange: #f6993f; $yellow: #ffed4a; $green: #38c172; $teal: #4dc0b5; $cyan: #6cb2eb; + +// App colors +$menu-bg-color: #f6f5f3; diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss index 9929213c..5f4c665e 100644 --- a/src/resources/sass/app.scss +++ b/src/resources/sass/app.scss @@ -1,59 +1,69 @@ // Fonts // Variables @import 'variables'; // Bootstrap @import '~bootstrap/scss/bootstrap'; // Toastr @import '~@deveodk/vue-toastr/dist/@deveodk/vue-toastr.css'; // Fixes Toastr incompatibility with Bootstrap .toast-container > .toast { opacity: 1; } @import 'menu'; nav + .container { margin-top: 120px; } +#app { + margin-bottom: 2rem; +} + #error-page { align-items: center; display: flex; justify-content: center; height: 100vh; color: #636b6f; .code { border-right: 2px solid; font-size: 26px; padding: 0 15px; } .message { font-size: 18px; padding: 0 15px; } } .app-loader { background-color: $body-bg; height: 100%; width: 100%; position: absolute; top: 0; left: 0; display: flex; align-items: center; justify-content: center; .spinner-border { width: 120px; height: 120px; border-width: 15px; color: #b2aa99; } } + +pre { + margin: 1rem 0; + padding: 1rem; + background-color: $menu-bg-color; +} diff --git a/src/resources/vue/components/App.vue b/src/resources/vue/components/App.vue index aeb89996..86ae36c2 100644 --- a/src/resources/vue/components/App.vue +++ b/src/resources/vue/components/App.vue @@ -1,40 +1,40 @@ diff --git a/src/resources/vue/components/Dashboard.vue b/src/resources/vue/components/Dashboard.vue index 34655fff..b4b7eb4c 100644 --- a/src/resources/vue/components/Dashboard.vue +++ b/src/resources/vue/components/Dashboard.vue @@ -1,85 +1,85 @@ diff --git a/src/resources/vue/components/Domain.vue b/src/resources/vue/components/Domain.vue new file mode 100644 index 00000000..15b539ee --- /dev/null +++ b/src/resources/vue/components/Domain.vue @@ -0,0 +1,88 @@ + + + diff --git a/src/resources/vue/components/Login.vue b/src/resources/vue/components/Login.vue index a4e49df1..7b234403 100644 --- a/src/resources/vue/components/Login.vue +++ b/src/resources/vue/components/Login.vue @@ -1,83 +1,83 @@ diff --git a/src/resources/vue/js/routes.js b/src/resources/vue/js/routes.js index 0e81ff1f..40c36ec4 100644 --- a/src/resources/vue/js/routes.js +++ b/src/resources/vue/js/routes.js @@ -1,76 +1,80 @@ import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) import DashboardComponent from '../components/Dashboard' +import DomainComponent from '../components/Domain' import Error404Component from '../components/404' import LoginComponent from '../components/Login' import LogoutComponent from '../components/Logout' import PasswordResetComponent from '../components/PasswordReset' import SignupComponent from '../components/Signup' import store from './store' const routes = [ { path: '/', redirect: { name: 'login' } }, { path: '/dashboard', name: 'dashboard', component: DashboardComponent, meta: { requiresAuth: true } }, + { + path: '/domain/:domain', + name: 'domain', + component: DomainComponent, + meta: { requiresAuth: true } + }, { path: '/login', name: 'login', component: LoginComponent }, { path: '/logout', name: 'logout', component: LogoutComponent }, { path: '/password-reset/:code?', name: 'password-reset', component: PasswordResetComponent }, { path: '/signup/:param?', name: 'signup', component: SignupComponent }, { name: '404', path: '*', component: Error404Component } ] const router = new VueRouter({ mode: 'history', routes }) router.beforeEach((to, from, next) => { - // check if the route requires authentication and user is not logged in if (to.matched.some(route => route.meta.requiresAuth) && !store.state.isLoggedIn) { + // remember the original request, to use after login + store.state.afterLogin = to; + // redirect to login page next({ name: 'login' }) - return - } - // if logged in redirect to dashboard - if (to.path === '/login' && store.state.isLoggedIn) { - next({ name: 'dashboard' }) return } next() }) export default router diff --git a/src/routes/api.php b/src/routes/api.php index d2d89493..b9a30739 100644 --- a/src/routes/api.php +++ b/src/routes/api.php @@ -1,48 +1,51 @@ 'api', 'prefix' => 'auth' ], function ($router) { Route::get('info', 'API\UsersController@info'); Route::post('login', 'API\UsersController@login'); Route::post('logout', 'API\UsersController@logout'); Route::post('refresh', 'API\UsersController@refresh'); Route::post('password-reset/init', 'API\PasswordResetController@init'); Route::post('password-reset/verify', 'API\PasswordResetController@verify'); Route::post('password-reset', 'API\PasswordResetController@reset'); Route::get('signup/plans', 'API\SignupController@plans'); Route::post('signup/init', 'API\SignupController@init'); Route::post('signup/verify', 'API\SignupController@verify'); Route::post('signup', 'API\SignupController@signup'); } ); Route::group( [ 'middleware' => 'auth:api', 'prefix' => 'v4' ], function () { + Route::apiResource('domains', API\DomainsController::class); + Route::get('domains/{id}/confirm', 'API\DomainsController@confirm'); + Route::apiResource('entitlements', API\EntitlementsController::class); Route::apiResource('users', API\UsersController::class); Route::apiResource('wallets', API\WalletsController::class); } ); diff --git a/src/tests/Browser/Components/Error.php b/src/tests/Browser/Components/Error.php new file mode 100644 index 00000000..2a70e294 --- /dev/null +++ b/src/tests/Browser/Components/Error.php @@ -0,0 +1,61 @@ + 'Not Found' + ]; + + public function __construct($code) + { + $this->code = $code; + $this->message = $this->messages_map[$code]; + } + + /** + * Get the root selector for the component. + * + * @return string + */ + public function selector() + { + return '#error-page'; + } + + /** + * Assert that the browser page contains the component. + * + * @param Browser $browser + * + * @return void + */ + public function assert(Browser $browser) + { + $browser->waitFor($this->selector()) + ->assertSeeIn('@code', $this->code) + ->assertSeeIn('@message', $this->message); + } + + /** + * Get the element shortcuts for the component. + * + * @return array + */ + public function elements() + { + $selector = $this->selector(); + + return [ + '@code' => "$selector .code", + '@message' => "$selector .message", + ]; + } +} diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php new file mode 100644 index 00000000..cbd34234 --- /dev/null +++ b/src/tests/Browser/DomainTest.php @@ -0,0 +1,86 @@ +browse(function (Browser $browser) { + $browser->visit('/domain/123')->on(new Home()); + }); + } + + /** + * Test domain info page (non-existing domain id) + */ + public function testDomainInfo404(): void + { + $this->browse(function (Browser $browser) { + // FIXME: I couldn't make loginAs() method working + + // Note: Here we're also testing that unauthenticated request + // is passed to logon form and then "redirected" to the requested page + $browser->visit('/domain/123') + ->on(new Home()) + ->submitLogon('john@kolab.org', 'simple123') + // TODO: the check below could look simpler, but we can't + // just remove the callback argument. We'll create + // Browser wrappen in future, then we could create expectError() method + ->with(new Error('404'), function (Browser $browser) { + }); + }); + } + + /** + * Test domain info page (existing domain) + * + * @depends testDomainInfo404 + */ + public function testDomainInfo(): void + { + $this->browse(function (Browser $browser) { + // Unconfirmed domain + $domain = Domain::where('namespace', 'kolab.org')->first(); + $domain->status ^= Domain::STATUS_CONFIRMED; + $domain->save(); + + $browser->visit('/domain/' . $domain->id) + ->on(new DomainPage()) + ->whenAvailable('@verify', function (Browser $browser) use ($domain) { + // Make sure the domain is confirmed now + // TODO: Test verification process failure + $domain->status |= Domain::STATUS_CONFIRMED; + $domain->save(); + + $browser->assertSeeIn('pre', $domain->namespace) + ->assertSeeIn('pre', $domain->hash()) + ->click('button'); + }) + ->whenAvailable('@config', function (Browser $browser) use ($domain) { + $browser->assertSeeIn('pre', $domain->namespace); + }) + ->assertMissing('@verify'); + + // Check that confirmed domain page contains only the config box + $browser->visit('/domain/' . $domain->id) + ->on(new DomainPage()) + ->assertMissing('@verify') + ->assertPresent('@config'); + }); + } +} diff --git a/src/tests/Browser/ErrorTest.php b/src/tests/Browser/ErrorTest.php index a92d4f4d..c15d7847 100644 --- a/src/tests/Browser/ErrorTest.php +++ b/src/tests/Browser/ErrorTest.php @@ -1,38 +1,36 @@ browse(function (Browser $browser) { $browser->visit('/unknown'); $browser->waitFor('#app > #error-page'); $browser->assertVisible('#app > #primary-menu'); $this->assertSame('404', $browser->text('#error-page .code')); $this->assertSame('Not Found', $browser->text('#error-page .message')); }); $this->browse(function (Browser $browser) { $browser->visit('/login/unknown'); $browser->waitFor('#app > #error-page'); $browser->assertVisible('#app > #primary-menu'); $this->assertSame('404', $browser->text('#error-page .code')); $this->assertSame('Not Found', $browser->text('#error-page .message')); }); - - // TODO: Test the same as above, but with use of Vue router } } diff --git a/src/tests/Browser/Pages/Domain.php b/src/tests/Browser/Pages/Domain.php new file mode 100644 index 00000000..e372c5bd --- /dev/null +++ b/src/tests/Browser/Pages/Domain.php @@ -0,0 +1,46 @@ +waitUntilMissing('@app .app-loader') + ->assertPresent('@config,@verify'); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements(): array + { + return [ + '@app' => '#app', + '@config' => '#domain-config', + '@verify' => '#domain-verify', + ]; + } +} diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php new file mode 100644 index 00000000..5e6f0ff7 --- /dev/null +++ b/src/tests/Feature/Controller/DomainsTest.php @@ -0,0 +1,112 @@ +delete(); + Domain::where('namespace', 'domainscontroller.com')->delete(); + } + + /** + * Test domain confirm request + */ + public function testConfirm(): void + { + $sku_domain = Sku::where('title', 'domain')->first(); + $user = $this->getTestUser('test1@domainscontroller.com'); + $domain = $this->getTestDomain('domainscontroller.com', [ + 'status' => Domain::STATUS_NEW, + 'type' => Domain::TYPE_EXTERNAL, + ]); + + // No entitlement (user has no access to this domain), expect 403 + $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); + $response->assertStatus(403); + + Entitlement::create([ + 'owner_id' => $user->id, + 'wallet_id' => $user->wallets()->first()->id, + 'sku_id' => $sku_domain->id, + 'entitleable_id' => $domain->id, + 'entitleable_type' => Domain::class + ]); + + $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertEquals('error', $json['status']); + + $domain->status |= Domain::STATUS_CONFIRMED; + $domain->save(); + + $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertEquals('success', $json['status']); + } + + /** + * Test fetching domain info + */ + public function testShow(): void + { + $sku_domain = Sku::where('title', 'domain')->first(); + $user = $this->getTestUser('test1@domainscontroller.com'); + $domain = $this->getTestDomain('domainscontroller.com', [ + 'status' => Domain::STATUS_NEW, + 'type' => Domain::TYPE_EXTERNAL, + ]); + + // No entitlement (user has no access to this domain), expect 403 + $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}"); + $response->assertStatus(403); + + Entitlement::create([ + 'owner_id' => $user->id, + 'wallet_id' => $user->wallets()->first()->id, + 'sku_id' => $sku_domain->id, + 'entitleable_id' => $domain->id, + 'entitleable_type' => Domain::class + ]); + + $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertEquals($domain->id, $json['id']); + $this->assertEquals($domain->namespace, $json['namespace']); + $this->assertEquals($domain->status, $json['status']); + $this->assertEquals($domain->type, $json['type']); + $this->assertTrue($json['confirmed'] === false); + $this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']); + $this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']); + $this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']); + $this->assertCount(4, $json['config']); + $this->assertTrue(strpos(implode("\n", $json['config']), $domain->namespace) !== false); + $this->assertCount(8, $json['dns']); + $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false); + $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false); + } +} diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php index ba8e58cf..0677b28a 100644 --- a/src/tests/Feature/Controller/UsersTest.php +++ b/src/tests/Feature/Controller/UsersTest.php @@ -1,145 +1,145 @@ delete(); Domain::where('namespace', 'userscontroller.com')->delete(); } /** * Test fetching current user info */ public function testInfo(): void { $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $response = $this->actingAs($user)->get("api/auth/info"); $json = $response->json(); $response->assertStatus(200); $this->assertEquals($user->id, $json['id']); $this->assertEquals($user->email, $json['email']); $this->assertEquals(User::STATUS_NEW, $json['status']); $this->assertTrue(is_array($json['statusInfo'])); } public function testIndex(): void { $userA = $this->getTestUser('UserEntitlement2A@UserEntitlement.com'); $response = $this->actingAs($userA, 'api')->get("/api/v4/users/{$userA->id}"); $response->assertStatus(200); $response->assertJson(['id' => $userA->id]); $user = factory(User::class)->create(); $response = $this->actingAs($user)->get("/api/v4/users/{$userA->id}"); $response->assertStatus(404); } public function testLogin(): void { // TODO $this->markTestIncomplete(); } public function testLogout(): void { // TODO $this->markTestIncomplete(); } public function testRefresh(): void { // TODO $this->markTestIncomplete(); } - public function testShow(): void + public function testStatusInfo(): void { $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $user->status = User::STATUS_NEW; $user->save(); $result = UsersController::statusInfo($user); $this->assertSame('new', $result['status']); $this->assertCount(3, $result['process']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('user-ldap-ready', $result['process'][1]['label']); $this->assertSame(false, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(false, $result['process'][2]['state']); $user->status |= User::STATUS_LDAP_READY | User::STATUS_IMAP_READY; $user->save(); $result = UsersController::statusInfo($user); $this->assertSame('new', $result['status']); $this->assertCount(3, $result['process']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('user-ldap-ready', $result['process'][1]['label']); $this->assertSame(true, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(true, $result['process'][2]['state']); $user->status |= User::STATUS_ACTIVE; $user->save(); - $domain->status |= Domain::STATUS_VERIFIED; +// $domain->status |= Domain::STATUS_VERIFIED; $domain->type = Domain::TYPE_EXTERNAL; $domain->save(); $result = UsersController::statusInfo($user); $this->assertSame('active', $result['status']); - $this->assertCount(7, $result['process']); + $this->assertCount(6, $result['process']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('user-ldap-ready', $result['process'][1]['label']); $this->assertSame(true, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(true, $result['process'][2]['state']); $this->assertSame('domain-new', $result['process'][3]['label']); $this->assertSame(true, $result['process'][3]['state']); $this->assertSame('domain-ldap-ready', $result['process'][4]['label']); $this->assertSame(false, $result['process'][4]['state']); - $this->assertSame('domain-verified', $result['process'][5]['label']); - $this->assertSame(true, $result['process'][5]['state']); - $this->assertSame('domain-confirmed', $result['process'][6]['label']); - $this->assertSame(false, $result['process'][6]['state']); +// $this->assertSame('domain-verified', $result['process'][5]['label']); +// $this->assertSame(true, $result['process'][5]['state']); + $this->assertSame('domain-confirmed', $result['process'][5]['label']); + $this->assertSame(false, $result['process'][5]['state']); $user->status |= User::STATUS_DELETED; $user->save(); $result = UsersController::statusInfo($user); $this->assertSame('deleted', $result['status']); } } diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php index 1d542cf0..db0de9fc 100644 --- a/src/tests/Feature/DomainTest.php +++ b/src/tests/Feature/DomainTest.php @@ -1,118 +1,167 @@ orWhere('namespace', 'gmail.com')->delete(); + $domains = [ + 'public-active.com', + 'gmail.com', + 'ci-success-cname.kolab.org', + 'ci-success-txt.kolab.org', + 'ci-failure-cname.kolab.org', + 'ci-failure-txt.kolab.org', + 'ci-failure-none.kolab.org', + ]; + + Domain::whereIn('namespace', $domains)->delete(); } /** * Test domain creating jobs */ public function testCreateJobs(): void { // Fake the queue, assert that no jobs were pushed... Queue::fake(); Queue::assertNothingPushed(); $domain = Domain::create([ 'namespace' => 'gmail.com', 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Queue::assertPushed(\App\Jobs\ProcessDomainCreate::class, 1); Queue::assertPushed(\App\Jobs\ProcessDomainCreate::class, function ($job) use ($domain) { $job_domain = TestCase::getObjectProperty($job, 'domain'); return $job_domain->id === $domain->id && $job_domain->namespace === $domain->namespace; }); - +/* Queue::assertPushedWithChain(\App\Jobs\ProcessDomainCreate::class, [ \App\Jobs\ProcessDomainVerify::class, ]); - +*/ /* FIXME: Looks like we can't really do detailed assertions on chained jobs Another thing to consider is if we maybe should run these jobs independently (not chained) and make sure there's no race-condition in status update Queue::assertPushed(\App\Jobs\ProcessDomainVerify::class, 1); Queue::assertPushed(\App\Jobs\ProcessDomainVerify::class, function ($job) use ($domain) { $job_domain = TestCase::getObjectProperty($job, 'domain'); return $job_domain->id === $domain->id && $job_domain->namespace === $domain->namespace; }); */ } /** * Tests getPublicDomains() method */ public function testGetPublicDomains(): void { $public_domains = Domain::getPublicDomains(); $this->assertNotContains('public-active.com', $public_domains); Queue::fake(); $domain = Domain::create([ 'namespace' => 'public-active.com', 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); // Public but non-active domain should not be returned $public_domains = Domain::getPublicDomains(); $this->assertNotContains('public-active.com', $public_domains); $domain = Domain::where('namespace', 'public-active.com')->first(); $domain->status = Domain::STATUS_ACTIVE; $domain->save(); // Public and active domain should be returned $public_domains = Domain::getPublicDomains(); $this->assertContains('public-active.com', $public_domains); } /** - * Test domain confirmation + * Test domain (ownership) confirmation * * @group dns */ public function testConfirm(): void { - // TODO - $this->markTestIncomplete(); - } + /* + DNS records for positive and negative tests - kolab.org: - /** - * Test domain verification - * - * @group dns - */ - public function testVerify(): void - { - // TODO - $this->markTestIncomplete(); + ci-success-cname A 212.103.80.148 + ci-success-cname MX 10 mx01.kolabnow.com. + ci-success-cname TXT "v=spf1 mx -all" + kolab-verify.ci-success-cname CNAME 2b719cfa4e1033b1e1e132977ed4fe3e.ci-success-cname + + ci-failure-cname A 212.103.80.148 + ci-failure-cname MX 10 mx01.kolabnow.com. + kolab-verify.ci-failure-cname CNAME 2b719cfa4e1033b1e1e132977ed4fe3e.ci-failure-cname + + ci-success-txt A 212.103.80.148 + ci-success-txt MX 10 mx01.kolabnow.com. + ci-success-txt TXT "v=spf1 mx -all" + ci-success-txt TXT "kolab-verify=de5d04ababb52d52e2519a2f16d11422" + + ci-failure-txt A 212.103.80.148 + ci-failure-txt MX 10 mx01.kolabnow.com. + kolab-verify.ci-failure-txt TXT "kolab-verify=de5d04ababb52d52e2519a2f16d11422" + + ci-failure-none A 212.103.80.148 + ci-failure-none MX 10 mx01.kolabnow.com. + */ + + Queue::fake(); + + $domain_props = ['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL]; + + $domain = $this->getTestDomain('ci-failure-none.kolab.org', $domain_props); + + $this->assertTrue($domain->confirm() === false); + $this->assertTrue(!$domain->isConfirmed()); + + $domain = $this->getTestDomain('ci-failure-txt.kolab.org', $domain_props); + + $this->assertTrue($domain->confirm() === false); + $this->assertTrue(!$domain->isConfirmed()); + + $domain = $this->getTestDomain('ci-failure-cname.kolab.org', $domain_props); + + $this->assertTrue($domain->confirm() === false); + $this->assertTrue(!$domain->isConfirmed()); + + $domain = $this->getTestDomain('ci-success-txt.kolab.org', $domain_props); + + $this->assertTrue($domain->confirm()); + $this->assertTrue($domain->isConfirmed()); + + $domain = $this->getTestDomain('ci-success-cname.kolab.org', $domain_props); + + $this->assertTrue($domain->confirm()); + $this->assertTrue($domain->isConfirmed()); } } diff --git a/src/tests/Unit/DomainTest.php b/src/tests/Unit/DomainTest.php index 246c2ad4..95b5d7d1 100644 --- a/src/tests/Unit/DomainTest.php +++ b/src/tests/Unit/DomainTest.php @@ -1,109 +1,117 @@ 'test.com', 'status' => \array_sum($domain_statuses), 'type' => Domain::TYPE_EXTERNAL ] ); $this->assertTrue($domain->isNew() === in_array(Domain::STATUS_NEW, $domain_statuses)); $this->assertTrue($domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domain_statuses)); $this->assertTrue($domain->isConfirmed() === in_array(Domain::STATUS_CONFIRMED, $domain_statuses)); $this->assertTrue($domain->isSuspended() === in_array(Domain::STATUS_SUSPENDED, $domain_statuses)); $this->assertTrue($domain->isDeleted() === in_array(Domain::STATUS_DELETED, $domain_statuses)); $this->assertTrue($domain->isLdapReady() === in_array(Domain::STATUS_LDAP_READY, $domain_statuses)); - $this->assertTrue($domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domain_statuses)); +// $this->assertTrue($domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domain_statuses)); } } /** * Test setStatusAttribute exception */ public function testDomainStatusInvalid(): void { $this->expectException(\Exception::class); $domain = new Domain( [ 'namespace' => 'test.com', 'status' => 1234567, ] ); } /** * Test basic Domain funtionality */ public function testDomainType(): void { $types = [ Domain::TYPE_PUBLIC, Domain::TYPE_HOSTED, Domain::TYPE_EXTERNAL, ]; $domains = \App\Utils::powerSet($types); foreach ($domains as $domain_types) { $domain = new Domain( [ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, 'type' => \array_sum($domain_types), ] ); $this->assertTrue($domain->isPublic() === in_array(Domain::TYPE_PUBLIC, $domain_types)); $this->assertTrue($domain->isHosted() === in_array(Domain::TYPE_HOSTED, $domain_types)); $this->assertTrue($domain->isExternal() === in_array(Domain::TYPE_EXTERNAL, $domain_types)); } } /** * Test domain hash generation */ public function testHash(): void { $domain = new Domain([ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, ]); - $hash1 = $domain->hash(true); + $hash_code = $domain->hash(); - $this->assertRegExp('/^[a-f0-9]{32}$/', $hash1); + $this->assertRegExp('/^[a-f0-9]{32}$/', $hash_code); - $hash2 = $domain->hash(); + $hash_text = $domain->hash(Domain::HASH_TEXT); - $this->assertRegExp('/^kolab-verify=[a-f0-9]{32}$/', $hash2); + $this->assertRegExp('/^kolab-verify=[a-f0-9]{32}$/', $hash_text); - $this->assertSame($hash1, str_replace('kolab-verify=', '', $hash2)); + $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); } }