diff --git a/src/.env.example b/src/.env.example --- a/src/.env.example +++ b/src/.env.example @@ -66,6 +66,11 @@ 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 diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -17,7 +17,7 @@ // ownership of the domain has been confirmed public const STATUS_CONFIRMED = 1 << 4; // domain has been verified that it exists in DNS - public const STATUS_VERIFIED = 1 << 5; +// public const STATUS_VERIFIED = 1 << 5; // domain has been created in LDAP public const STATUS_LDAP_READY = 1 << 6; @@ -28,6 +28,10 @@ // zone registered externally public const TYPE_EXTERNAL = 1 << 2; + public const HASH_CODE = 1; + public const HASH_TEXT = 2; + public const HASH_CNAME = 3; + public $incrementing = false; protected $keyType = 'bigint'; @@ -150,11 +154,12 @@ * * @return bool */ +/* public function isVerified(): bool { return $this->status & self::STATUS_VERIFIED; } - +*/ /** * Domain status mutator * @@ -171,7 +176,7 @@ self::STATUS_SUSPENDED, self::STATUS_DELETED, self::STATUS_LDAP_READY, - self::STATUS_VERIFIED, +// self::STATUS_VERIFIED, ]; foreach ($allowed_values as $value) { @@ -201,14 +206,14 @@ 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) { @@ -223,7 +228,7 @@ // 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) { @@ -249,15 +254,21 @@ /** * 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; } /** @@ -266,6 +277,7 @@ * @return bool True if registered, False otherwise * @throws \Exception Throws exception on DNS or DB errors */ +/* public function verify(): bool { if ($this->isVerified()) { @@ -287,4 +299,5 @@ return false; } +*/ } diff --git a/src/app/Http/Controllers/API/DomainsController.php b/src/app/Http/Controllers/API/DomainsController.php new file mode 100644 --- /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 --- a/src/app/Http/Controllers/API/UsersController.php +++ b/src/app/Http/Controllers/API/UsersController.php @@ -208,7 +208,7 @@ if (!$domain->isPublic()) { $steps['domain-new'] = true; $steps['domain-ldap-ready'] = 'isLdapReady'; - $steps['domain-verified'] = 'isVerified'; +// $steps['domain-verified'] = 'isVerified'; $steps['domain-confirmed'] = 'isConfirmed'; } @@ -219,7 +219,7 @@ $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']) { diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php --- a/src/app/Observers/DomainObserver.php +++ b/src/app/Observers/DomainObserver.php @@ -36,11 +36,14 @@ 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); } /** diff --git a/src/app/Sku.php b/src/app/Sku.php --- a/src/app/Sku.php +++ b/src/app/Sku.php @@ -52,6 +52,11 @@ */ 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(); @@ -76,7 +81,7 @@ 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."); } } diff --git a/src/config/dns.php b/src/config/dns.php new file mode 100644 --- /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 --- a/src/database/seeds/SkuSeeder.php +++ b/src/database/seeds/SkuSeeder.php @@ -54,7 +54,7 @@ 'units_free' => 1, 'period' => 'monthly', 'handler_class' => 'App\Handlers\DomainHosting', - 'active' => false, + 'active' => true, ] ); diff --git a/src/phpunit.xml b/src/phpunit.xml --- a/src/phpunit.xml +++ b/src/phpunit.xml @@ -10,11 +10,12 @@ stopOnFailure="false"> - ./tests/Unit + tests/Unit - ./tests/Feature + tests/Feature + tests/Feature/Jobs/ProcessDomainVerifyTest.php diff --git a/src/resources/js/app.js b/src/resources/js/app.js --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -85,11 +85,16 @@ $(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() { diff --git a/src/resources/sass/_variables.scss b/src/resources/sass/_variables.scss --- a/src/resources/sass/_variables.scss +++ b/src/resources/sass/_variables.scss @@ -17,3 +17,6 @@ $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 --- a/src/resources/sass/app.scss +++ b/src/resources/sass/app.scss @@ -20,6 +20,10 @@ margin-top: 120px; } +#app { + margin-bottom: 2rem; +} + #error-page { align-items: center; display: flex; @@ -57,3 +61,9 @@ 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 --- a/src/resources/vue/components/App.vue +++ b/src/resources/vue/components/App.vue @@ -21,7 +21,7 @@ this.$store.state.authInfo = response.data this.isLoading = false this.$root.stopLoading() - this.$root.loginUser(token) + this.$root.loginUser(token, false) }) .catch(error => { this.isLoading = false diff --git a/src/resources/vue/components/Dashboard.vue b/src/resources/vue/components/Dashboard.vue --- a/src/resources/vue/components/Dashboard.vue +++ b/src/resources/vue/components/Dashboard.vue @@ -4,7 +4,7 @@
Dashboard
-

{{ data }}

+
{{ data }}
diff --git a/src/resources/vue/components/Domain.vue b/src/resources/vue/components/Domain.vue new file mode 100644 --- /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 --- a/src/resources/vue/components/Login.vue +++ b/src/resources/vue/components/Login.vue @@ -22,12 +22,12 @@ return { email: '', password: '', - loginError: false, + loginError: false } }, methods: { submitLogin() { - this.loginError = false; + this.loginError = false axios.post('/api/auth/login', { email: this.email, password: this.password diff --git a/src/resources/vue/js/routes.js b/src/resources/vue/js/routes.js --- a/src/resources/vue/js/routes.js +++ b/src/resources/vue/js/routes.js @@ -4,6 +4,7 @@ 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' @@ -24,6 +25,12 @@ meta: { requiresAuth: true } }, { + path: '/domain/:domain', + name: 'domain', + component: DomainComponent, + meta: { requiresAuth: true } + }, + { path: '/login', name: 'login', component: LoginComponent @@ -56,17 +63,14 @@ }) 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 } diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -41,6 +41,9 @@ '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 --- /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 --- /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 --- a/src/tests/Browser/ErrorTest.php +++ b/src/tests/Browser/ErrorTest.php @@ -32,7 +32,5 @@ $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 --- /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 --- /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 --- a/src/tests/Feature/Controller/UsersTest.php +++ b/src/tests/Feature/Controller/UsersTest.php @@ -74,7 +74,7 @@ $this->markTestIncomplete(); } - public function testShow(): void + public function testStatusInfo(): void { $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ @@ -112,14 +112,14 @@ $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']); @@ -130,10 +130,10 @@ $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(); diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php --- a/src/tests/Feature/DomainTest.php +++ b/src/tests/Feature/DomainTest.php @@ -17,8 +17,17 @@ { parent::setUp(); - Domain::where('namespace', 'public-active.com') - ->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(); } /** @@ -43,11 +52,11 @@ 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 @@ -95,24 +104,64 @@ } /** - * 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 --- a/src/tests/Unit/DomainTest.php +++ b/src/tests/Unit/DomainTest.php @@ -19,7 +19,7 @@ Domain::STATUS_SUSPENDED, Domain::STATUS_DELETED, Domain::STATUS_LDAP_READY, - Domain::STATUS_VERIFIED, +// Domain::STATUS_VERIFIED, ]; $domains = \App\Utils::powerSet($statuses); @@ -39,7 +39,7 @@ $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)); } } @@ -96,14 +96,22 @@ '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); } }