Page MenuHomePhorge

D937.1775362197.diff
No OneTemporary

Authored By
Unknown
Size
38 KB
Referenced Files
None
Subscribers
None

D937.1775362197.diff

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 <hash>.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 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Domain;
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class DomainsController extends Controller
+{
+ /**
+ * Display a listing of the resource.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function index()
+ {
+ //
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function create()
+ {
+ //
+ }
+
+ /**
+ * Confirm ownership of the specified domain (via DNS check).
+ *
+ * @param int $id Domain identifier
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function confirm($id)
+ {
+ $domain = Domain::findOrFail($id);
+
+ // Only owner (or admin) has access to the domain
+ if (!self::hasAccess($domain)) {
+ return abort(403);
+ }
+
+ if (!$domain->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 <some-ip>",
+ "www IN A <some-ip>",
+ ";",
+ "{$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';
}
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/config/dns.php b/src/config/dns.php
new file mode 100644
--- /dev/null
+++ b/src/config/dns.php
@@ -0,0 +1,8 @@
+<?php
+
+return [
+ 'ttl' => env('DNS_TTL', 3600),
+ 'spf' => env('DNS_SPF', null),
+ 'static' => env('DNS_STATIC', null),
+ 'copyfrom' => env('DNS_COPY_FROM', null),
+];
diff --git a/src/phpunit.xml b/src/phpunit.xml
--- a/src/phpunit.xml
+++ b/src/phpunit.xml
@@ -10,11 +10,12 @@
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
- <directory suffix="Test.php">./tests/Unit</directory>
+ <directory suffix="Test.php">tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
- <directory suffix="Test.php">./tests/Feature</directory>
+ <directory suffix="Test.php">tests/Feature</directory>
+ <exclude>tests/Feature/Jobs/ProcessDomainVerifyTest.php</exclude>
</testsuite>
</testsuites>
<filter>
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 @@
<div class="card-body">
<div class="card-title">Dashboard</div>
<div class="card-text">
- <p>{{ data }}</p>
+ <pre>{{ data }}</pre>
</div>
</div>
</div>
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 @@
+<template>
+ <div class="container">
+ <div v-if="domain && !is_confirmed" class="card" id="domain-verify">
+ <div class="card-body">
+ <div class="card-title">Domain verification</div>
+ <div class="card-text">
+ <p>In order to confirm that you're the actual holder of the domain,
+ we need to run a verification process before finally activating it for email delivery.</p>
+ <p>The domain <b>must have one of the following entries</b> in DNS:
+ <ul>
+ <li>TXT entry with value: <code>{{ domain.hash_text }}</code></li>
+ <li>or CNAME entry: <code>{{ domain.hash_cname }}.{{ domain.namespace }}. IN CNAME {{ domain.hash_code }}.{{ domain.namespace }}.</code></li>
+ </ul>
+ When this is done press the button below to start the verification.</p>
+ <p>Here's a sample zone file for your domain:
+ <pre>{{ domain.dns.join("\n") }}</pre>
+ </p>
+ <button class="btn btn-primary" type="button" @click="confirm">Verify</button>
+ </div>
+ </div>
+ </div>
+ <div v-if="domain && is_confirmed" class="card" id="domain-config">
+ <div class="card-body">
+ <div class="card-title">Domain configuration</div>
+ <div class="card-text">
+ <p>In order to let {{ app_name }} receive email traffic for your domain you need to adjust
+ the DNS settings, more precisely the MX entries, accordingly.</p>
+ <p>Edit your domain's zone file and replace existing MX
+ entries with the following values:
+ <pre>{{ domain.config.join("\n") }}</pre>
+ </p>
+ <p>If you don't know how to set DNS entries for your domain,
+ please contact the registration service where you registered
+ the domain or your web hosting provider.
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ import store from '../js/store'
+
+ export default {
+ data() {
+ return {
+ domain_id: null,
+ domain: null,
+ is_confirmed: false,
+ app_name: window.config['app.name']
+ }
+ },
+ created() {
+ if (this.domain_id = this.$route.params.domain) {
+ axios.get('/api/v4/domains/' + this.domain_id)
+ .then(response => {
+ this.is_confirmed = response.data.confirmed
+ this.domain = response.data
+ if (!this.is_confirmed) {
+ $('#domain-verify button').focus()
+ }
+ })
+ .catch(error => {
+ // TODO: In place like this we should handle any errors
+ // with an error page.
+ if (error.response && error.response.status == 404) {
+ this.$router.replace({name: '404'})
+ }
+ })
+ } else {
+ // TODO: Find a way to display error page without changing the URL
+ // Maybe https://github.com/raniesantos/vue-error-page
+ this.$router.push({name: '404'})
+ }
+ },
+ methods: {
+ confirm() {
+ axios.get('/api/v4/domains/' + this.domain_id + '/confirm')
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.is_confirmed = true
+ }
+ })
+ }
+ }
+ }
+</script>
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'
@@ -23,6 +24,12 @@
component: DashboardComponent,
meta: { requiresAuth: true }
},
+ {
+ path: '/domain/:domain',
+ name: 'domain',
+ component: DomainComponent,
+ meta: { requiresAuth: true }
+ },
{
path: '/login',
name: 'login',
@@ -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 @@
+<?php
+
+namespace Tests\Browser\Components;
+
+use Laravel\Dusk\Browser;
+use Laravel\Dusk\Component as BaseComponent;
+use PHPUnit\Framework\Assert as PHPUnit;
+
+class Error extends BaseComponent
+{
+ protected $code;
+ protected $message;
+ protected $messages_map = [
+ 404 => '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 @@
+<?php
+
+namespace Tests\Browser;
+
+use App\Domain;
+use App\User;
+use Tests\Browser\Components\Error;
+use Tests\Browser\Pages\Domain as DomainPage;
+use Tests\Browser\Pages\Home;
+use Tests\DuskTestCase;
+use Laravel\Dusk\Browser;
+use Illuminate\Foundation\Testing\DatabaseMigrations;
+
+class DomainTest extends DuskTestCase
+{
+
+ /**
+ * Test domain info page (unauthenticated)
+ */
+ public function testDomainInfoUnauth(): void
+ {
+ // Test that the page requires authentication
+ $this->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 @@
+<?php
+
+namespace Tests\Browser\Pages;
+
+use Laravel\Dusk\Browser;
+use Laravel\Dusk\Page;
+
+class Domain extends Page
+{
+ /**
+ * Get the URL for the page.
+ *
+ * @return string
+ */
+ public function url(): string
+ {
+ return '';
+ }
+
+ /**
+ * Assert that the browser is on the page.
+ *
+ * @param \Laravel\Dusk\Browser $browser
+ *
+ * @return void
+ */
+ public function assert(Browser $browser)
+ {
+ $browser->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 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Http\Controllers\API\DomainsController;
+use App\Domain;
+use App\Entitlement;
+use App\Sku;
+use App\User;
+use App\Wallet;
+use Illuminate\Support\Str;
+use Tests\TestCase;
+
+class DomainsTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ User::where('email', 'test1@domainscontroller.com')->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);
}
}

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 4:09 AM (16 h, 20 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18832300
Default Alt Text
D937.1775362197.diff (38 KB)

Event Timeline