Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117382293
D937.1774822101.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
40 KB
Referenced Files
None
Subscribers
None
D937.1774822101.diff
View Options
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';
}
@@ -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 @@
+<?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/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">
<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'
@@ -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 @@
+<?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
Details
Attached
Mime Type
text/plain
Expires
Sun, Mar 29, 10:08 PM (5 d, 3 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18777096
Default Alt Text
D937.1774822101.diff (40 KB)
Attached To
Mode
D937: Domain verification/information UI and API
Attached
Detach File
Event Timeline