Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117822719
D3029.1775296643.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
26 KB
Referenced Files
None
Subscribers
None
D3029.1775296643.diff
View Options
diff --git a/src/.env.example b/src/.env.example
--- a/src/.env.example
+++ b/src/.env.example
@@ -161,6 +161,10 @@
#PASSPORT_PROXY_OAUTH_CLIENT_ID=
#PASSPORT_PROXY_OAUTH_CLIENT_SECRET=
+# Generate with ./artisan passport:client --password
+#PASSPORT_COMPANIONAPP_OAUTH_CLIENT_ID=
+#PASSPORT_COMPANIONAPP_OAUTH_CLIENT_SECRET=
+
PASSPORT_PRIVATE_KEY=
PASSPORT_PUBLIC_KEY=
diff --git a/src/app/Http/Controllers/API/V4/CompanionAppsController.php b/src/app/Http/Controllers/API/V4/CompanionAppsController.php
--- a/src/app/Http/Controllers/API/V4/CompanionAppsController.php
+++ b/src/app/Http/Controllers/API/V4/CompanionAppsController.php
@@ -2,11 +2,14 @@
namespace App\Http\Controllers\API\V4;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\ResourceController;
+use App\Utils;
+use App\Tenant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
+use BaconQrCode;
-class CompanionAppsController extends Controller
+class CompanionAppsController extends ResourceController
{
/**
* Register a companion app.
@@ -24,6 +27,7 @@
[
'notificationToken' => 'required|min:4|max:512',
'deviceId' => 'required|min:4|max:64',
+ 'name' => 'required|max:512',
]
);
@@ -33,8 +37,9 @@
$notificationToken = $request->notificationToken;
$deviceId = $request->deviceId;
+ $name = $request->name;
- \Log::info("Registering app. Notification token: {$notificationToken} Device id: {$deviceId}");
+ \Log::info("Registering app. Notification token: {$notificationToken} Device id: {$deviceId} Name: {$name}");
$app = \App\CompanionApp::where('device_id', $deviceId)->first();
if (!$app) {
@@ -42,6 +47,7 @@
$app->user_id = $user->id;
$app->device_id = $deviceId;
$app->mfa_enabled = true;
+ $app->name = $name;
} else {
//FIXME this allows a user to probe for another users deviceId
if ($app->user_id != $user->id) {
@@ -55,4 +61,139 @@
return response()->json(['status' => 'success']);
}
+
+
+ /**
+ * Generate a QR-code image for a string
+ *
+ * @param string $data data to encode
+ *
+ * @return string
+ */
+ private static function generateQRCode($data)
+ {
+ $renderer_style = new BaconQrCode\Renderer\RendererStyle\RendererStyle(300, 1);
+ $renderer_image = new BaconQrCode\Renderer\Image\SvgImageBackEnd();
+ $renderer = new BaconQrCode\Renderer\ImageRenderer($renderer_style, $renderer_image);
+ $writer = new BaconQrCode\Writer($renderer);
+
+ return 'data:image/svg+xml;base64,' . base64_encode($writer->writeString($data));
+ }
+
+ /**
+ * Delete a companion app.
+ *
+ * @param string $id Resource identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function destroy($id)
+ {
+ $result = \App\CompanionApp::find($id);
+ if (!$result) {
+ return $this->errorResponse(404);
+ }
+
+ $user = $this->guard()->user();
+ if ($user->id != $result->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ $result->delete();
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.companion-delete-success"),
+ ]);
+ }
+
+ /**
+ * List devices.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $user = $this->guard()->user();
+ $search = trim(request()->input('search'));
+ $page = intval(request()->input('page')) ?: 1;
+ $pageSize = 20;
+ $hasMore = false;
+
+ $result = \App\CompanionApp::where('user_id', $user->id);
+
+ $result = $result->orderBy('created_at')
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1))
+ ->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+
+ // Process the result
+ $result = $result->map(
+ function ($device) {
+ return $device->toArray();
+ }
+ );
+
+ $result = [
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => $hasMore,
+ ];
+
+ return response()->json($result);
+ }
+
+ /**
+ * Get the information about the specified companion app.
+ *
+ * @param string $id CompanionApp identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function show($id)
+ {
+ $result = \App\CompanionApp::find($id);
+ if (!$result) {
+ return $this->errorResponse(404);
+ }
+
+ $user = $this->guard()->user();
+ if ($user->id != $result->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ return response()->json($result->toArray());
+ }
+
+ /**
+ * Retrieve the pairing information encoded into a qrcode image.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function pairing()
+ {
+ $user = $this->guard()->user();
+
+ $clientIdentifier = \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_id');
+ $clientSecret = \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_secret');
+ if (empty($clientIdentifier) || empty($clientSecret)) {
+ \Log::warning("Empty client identifier or secret. Can't generate qr-code.");
+ return $this->errorResponse(500);
+ }
+
+ $response['qrcode'] = self::generateQRCode(
+ json_encode([
+ "serverUrl" => Utils::serviceUrl('', $user->tenant_id),
+ "clientIdentifier" => \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_id'),
+ "clientSecret" => \App\Tenant::getConfig($user->tenant_id, 'auth.companion_app.client_secret'),
+ "username" => $user->email
+ ])
+ );
+
+ return response()->json($response);
+ }
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -192,6 +192,7 @@
'enableSettings' => $isController,
'enableUsers' => $isController,
'enableWallets' => $isController,
+ 'enableCompanionapps' => $isController && in_array('beta', $skus),
];
return array_merge($process, $result);
diff --git a/src/composer.json b/src/composer.json
--- a/src/composer.json
+++ b/src/composer.json
@@ -15,6 +15,7 @@
],
"require": {
"php": "^7.3",
+ "bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-dompdf": "^0.8.6",
"doctrine/dbal": "^2.13",
"dyrynda/laravel-nullable-fields": "*",
diff --git a/src/config/auth.php b/src/config/auth.php
--- a/src/config/auth.php
+++ b/src/config/auth.php
@@ -118,6 +118,11 @@
'client_secret' => env('PASSPORT_PROXY_OAUTH_CLIENT_SECRET'),
],
+ 'companion_app' => [
+ 'client_id' => env('PASSPORT_COMPANIONAPP_OAUTH_CLIENT_ID'),
+ 'client_secret' => env('PASSPORT_COMPANIONAPP_OAUTH_CLIENT_SECRET'),
+ ],
+
'token_expiry_minutes' => env('OAUTH_TOKEN_EXPIRY', 60),
'refresh_token_expiry_minutes' => env('OAUTH_REFRESH_TOKEN_EXPIRY', 30 * 24 * 60),
];
diff --git a/src/database/seeds/local/OauthClientSeeder.php b/src/database/seeds/local/OauthClientSeeder.php
--- a/src/database/seeds/local/OauthClientSeeder.php
+++ b/src/database/seeds/local/OauthClientSeeder.php
@@ -30,5 +30,20 @@
$client->id = \config('auth.proxy.client_id');
$client->save();
+
+ $companionAppClient = Passport::client()->forceFill([
+ 'user_id' => null,
+ 'name' => "CompanionApp Password Grant Client",
+ 'secret' => \config('auth.companion_app.client_secret'),
+ 'provider' => 'users',
+ 'redirect' => 'https://' . \config('app.website_domain'),
+ 'personal_access_client' => 0,
+ 'password_client' => 1,
+ 'revoked' => false,
+ ]);
+
+ $companionAppClient->id = \config('auth.companion_app.client_id');
+
+ $companionAppClient->save();
}
}
diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js
--- a/src/resources/js/fontawesome.js
+++ b/src/resources/js/fontawesome.js
@@ -21,8 +21,9 @@
faUniversity,
faExclamationCircle,
faInfoCircle,
- faLock,
faKey,
+ faLock,
+ faMobile,
faPlus,
faSearch,
faSignInAlt,
@@ -57,8 +58,9 @@
faFolderOpen,
faGlobe,
faInfoCircle,
- faLock,
faKey,
+ faLock,
+ faMobile,
faPlus,
faSearch,
faSignInAlt,
diff --git a/src/resources/js/user/routes.js b/src/resources/js/user/routes.js
--- a/src/resources/js/user/routes.js
+++ b/src/resources/js/user/routes.js
@@ -8,6 +8,7 @@
// Note: you can pack multiple components into the same chunk, webpackChunkName
// is also used to get a sensible file name instead of numbers
+const CompanionAppComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/CompanionApp')
const DashboardComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Dashboard')
const DistlistInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Distlist/Info')
const DistlistListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Distlist/List')
@@ -45,6 +46,12 @@
component: DistlistListComponent,
meta: { requiresAuth: true, perm: 'distlists' }
},
+ {
+ path: '/companion',
+ name: 'companion',
+ component: CompanionAppComponent,
+ meta: { requiresAuth: true, perm: 'companionapps' }
+ },
{
path: '/domain/:domain',
name: 'domain',
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -19,6 +19,8 @@
'chart-income' => 'Income in :currency - last 8 weeks',
'chart-users' => 'Users - last 8 weeks',
+ 'companion-delete-success' => 'The companion app has been removed.',
+
'mandate-delete-success' => 'The auto-payment has been removed.',
'mandate-update-success' => 'The auto-payment has been updated.',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -38,10 +38,23 @@
'verify' => "Verify",
],
+ 'companion' => [
+ 'title' => "Companion App",
+ 'name' => "Name",
+ 'description' => "Use the Companion App on your mobile phone for advanced two factor authentication.",
+ 'pair-new' => "Pair new device",
+ 'paired' => "Paired devices",
+ 'pairing-instructions' => "Pair a new device using the following QR-Code:",
+ 'deviceid' => "Device ID",
+ 'nodevices' => "There are currently no devices",
+ 'forget-device' => "Forget Device"
+ ],
+
'dashboard' => [
'beta' => "beta",
'distlists' => "Distribution lists",
'chat' => "Video chat",
+ 'companion' => "Companion app",
'domains' => "Domains",
'invitations' => "Invitations",
'profile' => "Your profile",
diff --git a/src/resources/vue/CompanionApp.vue b/src/resources/vue/CompanionApp.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/CompanionApp.vue
@@ -0,0 +1,78 @@
+<template>
+ <div class="container" dusk="companionapp-component">
+ <div class="card">
+ <div class="card-body">
+ <div class="card-title">
+ <small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
+ {{ $t('companion.title') }}
+ </div>
+ <div class="card-text">
+ <p>
+ {{ $t('companion.description') }}
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <ul class="nav nav-tabs mt-2" role="tablist">
+ <li class="nav-item">
+ <a class="nav-link active" id="tab-qrcode" href="#companion-qrcode" role="tab" aria-controls="companion-qrcode" aria-selected="true" @click="$root.tab">
+ {{ $t('companion.pair-new') }}
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-list" href="#companion-list" role="tab" aria-controls="companion-list" aria-selected="false" @click="$root.tab">
+ {{ $t('companion.paired') }}
+ </a>
+ </li>
+ </ul>
+
+ <div class="tab-content">
+ <div class="tab-pane active" id="companion-qrcode" role="tabpanel" aria-labelledby="tab-qrcode">
+ <div class="card-body">
+ <div class="card-text">
+ <p>
+ {{ $t('companion.pairing-instructions') }}
+ </p>
+ <p>
+ <img :src="qrcode" />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="companion-list" role="tabpanel" aria-labelledby="tab-list">
+ <div class="card-body">
+ <companionapp-list class="card-text"></companionapp-list>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</template>
+
+<script>
+ import CompanionappList from './Widgets/CompanionappList'
+
+ export default {
+ components: {
+ CompanionappList
+ },
+ data() {
+ return {
+ qrcode: ""
+ }
+ },
+ mounted() {
+ this.$root.startLoading()
+
+ axios.get('/api/v4/companion/pairing')
+ .then(response => {
+ this.$root.stopLoading()
+ this.qrcode = response.data.qrcode
+ })
+ .catch(this.$root.errorHandler)
+ },
+ methods: {
+ },
+ }
+</script>
diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue
--- a/src/resources/vue/Dashboard.vue
+++ b/src/resources/vue/Dashboard.vue
@@ -38,6 +38,10 @@
<a v-if="webmailURL" class="card link-webmail" :href="webmailURL">
<svg-icon icon="envelope"></svg-icon><span class="name">{{ $t('dashboard.webmail') }}</span>
</a>
+ <router-link v-if="status.enableCompanionapps" class="card link-companionapp" :to="{ name: 'companion' }">
+ <svg-icon icon="mobile"></svg-icon><span class="name">{{ $t('dashboard.companion') }}</span>
+ <span class="badge bg-primary">{{ $t('dashboard.beta') }}</span>
+ </router-link>
</div>
</div>
</template>
diff --git a/src/resources/vue/Widgets/CompanionappList.vue b/src/resources/vue/Widgets/CompanionappList.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/CompanionappList.vue
@@ -0,0 +1,59 @@
+<template>
+ <div>
+ <table class="table table-sm m-0 entries">
+ <thead>
+ <tr>
+ <th scope="col">{{ $t('companion.name') }}</th>
+ <th scope="col">{{ $t('companion.deviceid') }}</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="entry in entries" :id="'entry' + entry.id" :key="entry.id">
+ <td class="description">{{ entry.name }}</td>
+ <td class="description">{{ entry.device_id }}</td>
+ <td class="selection">
+ <button class="btn btn-lg btn-link btn-action" title="$t('companion.forget-device')" type="button" @click="remove(entry.id)">
+ <svg-icon icon="trash-alt"></svg-icon>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ <list-foot :text="$t('companion.nodevices')" :colspan="3"></list-foot>
+ </table>
+ <list-more v-if="hasMore" :on-click="loadMore"></list-more>
+ </div>
+</template>
+
+<script>
+ import ListTools from './ListTools'
+
+ export default {
+ mixins: [ ListTools ],
+ props: {
+ },
+ data() {
+ return {
+ entries: []
+ }
+ },
+ mounted() {
+ this.loadMore({ reset: true })
+ },
+ methods: {
+ loadMore(params) {
+ this.listSearch('entries', '/api/v4/companion/', params)
+ },
+ remove(id) {
+ axios.delete('/api/v4/companion/' + id)
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$toast.success(response.data.message)
+ }
+ this.loadMore({ reset: true })
+ })
+ .catch(this.$root.errorHandler)
+ },
+ }
+ }
+</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -63,13 +63,15 @@
'prefix' => $prefix . 'api/v4'
],
function () {
- Route::post('companion/register', 'API\V4\CompanionAppsController@register');
-
Route::post('auth-attempts/{id}/confirm', 'API\V4\AuthAttemptsController@confirm');
Route::post('auth-attempts/{id}/deny', 'API\V4\AuthAttemptsController@deny');
Route::get('auth-attempts/{id}/details', 'API\V4\AuthAttemptsController@details');
Route::get('auth-attempts', 'API\V4\AuthAttemptsController@index');
+ Route::get('companion/pairing', 'API\V4\CompanionAppsController@pairing');
+ Route::apiResource('companion', 'API\V4\CompanionAppsController');
+ Route::post('companion/register', 'API\V4\CompanionAppsController@register');
+
Route::apiResource('domains', 'API\V4\DomainsController');
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
Route::get('domains/{id}/skus', 'API\V4\SkusController@domainSkus');
diff --git a/src/tests/Feature/Controller/CompanionAppsTest.php b/src/tests/Feature/Controller/CompanionAppsTest.php
--- a/src/tests/Feature/Controller/CompanionAppsTest.php
+++ b/src/tests/Feature/Controller/CompanionAppsTest.php
@@ -15,8 +15,9 @@
{
parent::setUp();
- $this->deleteTestUser('UsersControllerTest1@userscontroller.com');
- $this->deleteTestDomain('userscontroller.com');
+ $this->deleteTestUser('CompanionAppsTest1@userscontroller.com');
+ $this->deleteTestUser('CompanionAppsTest2@userscontroller.com');
+ $this->deleteTestCompanionApp('testdevice');
}
/**
@@ -24,8 +25,9 @@
*/
public function tearDown(): void
{
- $this->deleteTestUser('UsersControllerTest1@userscontroller.com');
- $this->deleteTestDomain('userscontroller.com');
+ $this->deleteTestUser('CompanionAppsTest1@userscontroller.com');
+ $this->deleteTestUser('CompanionAppsTest2@userscontroller.com');
+ $this->deleteTestCompanionApp('testdevice');
parent::tearDown();
}
@@ -39,10 +41,11 @@
$notificationToken = "notificationToken";
$deviceId = "deviceId";
+ $name = "testname";
$response = $this->actingAs($user)->post(
"api/v4/companion/register",
- ['notificationToken' => $notificationToken, 'deviceId' => $deviceId]
+ ['notificationToken' => $notificationToken, 'deviceId' => $deviceId, 'name' => $name]
);
$response->assertStatus(200);
@@ -50,13 +53,14 @@
$companionApp = \App\CompanionApp::where('device_id', $deviceId)->first();
$this->assertTrue($companionApp != null);
$this->assertEquals($deviceId, $companionApp->device_id);
+ $this->assertEquals($name, $companionApp->name);
$this->assertEquals($notificationToken, $companionApp->notification_token);
// Test a token update
$notificationToken = "notificationToken2";
$response = $this->actingAs($user)->post(
"api/v4/companion/register",
- ['notificationToken' => $notificationToken, 'deviceId' => $deviceId]
+ ['notificationToken' => $notificationToken, 'deviceId' => $deviceId, 'name' => $name]
);
$response->assertStatus(200);
@@ -75,8 +79,102 @@
$user2 = $this->getTestUser('CompanionAppsTest2@userscontroller.com');
$response = $this->actingAs($user2)->post(
"api/v4/companion/register",
- ['notificationToken' => $notificationToken, 'deviceId' => $deviceId]
+ ['notificationToken' => $notificationToken, 'deviceId' => $deviceId, 'name' => $name]
);
$response->assertStatus(403);
}
+
+ public function testIndex(): void
+ {
+ $response = $this->get("api/v4/companion");
+ $response->assertStatus(401);
+
+ $user = $this->getTestUser('CompanionAppsTest1@userscontroller.com');
+
+ $companionApp = $this->getTestCompanionApp(
+ 'testdevice',
+ $user,
+ [
+ 'notification_token' => 'notificationtoken',
+ 'mfa_enabled' => 1,
+ 'name' => 'testname',
+ ]
+ );
+
+ $response = $this->actingAs($user)->get("api/v4/companion");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($user->id, $json['list'][0]['user_id']);
+ $this->assertSame($companionApp['device_id'], $json['list'][0]['device_id']);
+ $this->assertSame($companionApp['name'], $json['list'][0]['name']);
+ $this->assertSame($companionApp['notification_token'], $json['list'][0]['notification_token']);
+ $this->assertSame($companionApp['mfa_enabled'], $json['list'][0]['mfa_enabled']);
+
+ $user2 = $this->getTestUser('CompanionAppsTest2@userscontroller.com');
+ $response = $this->actingAs($user2)->get(
+ "api/v4/companion"
+ );
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $this->assertSame(0, $json['count']);
+ $this->assertCount(0, $json['list']);
+ }
+
+ public function testShow(): void
+ {
+ $user = $this->getTestUser('CompanionAppsTest1@userscontroller.com');
+ $companionApp = $this->getTestCompanionApp('testdevice', $user);
+
+ $response = $this->get("api/v4/companion/{$companionApp->id}");
+ $response->assertStatus(401);
+
+ $response = $this->actingAs($user)->get("api/v4/companion/aaa");
+ $response->assertStatus(404);
+
+ $response = $this->actingAs($user)->get("api/v4/companion/{$companionApp->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $this->assertSame($companionApp->id, $json['id']);
+
+ $user2 = $this->getTestUser('CompanionAppsTest2@userscontroller.com');
+ $response = $this->actingAs($user2)->get("api/v4/companion/{$companionApp->id}");
+ $response->assertStatus(403);
+ }
+
+ public function testPairing(): void
+ {
+ $response = $this->get("api/v4/companion/pairing");
+ $response->assertStatus(401);
+
+ $user = $this->getTestUser('CompanionAppsTest1@userscontroller.com');
+ $response = $this->actingAs($user)->get("api/v4/companion/pairing");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $this->assertArrayHasKey('qrcode', $json);
+ $this->assertSame('data:image/svg+xml;base64,', substr($json['qrcode'], 0, 26));
+ }
+
+ public function testDelete(): void
+ {
+ $user = $this->getTestUser('CompanionAppsTest1@userscontroller.com');
+ $companionApp = $this->getTestCompanionApp('testdevice', $user);
+
+ $response = $this->delete("api/v4/companion/{$companionApp->id}");
+ $response->assertStatus(401);
+
+ $response = $this->actingAs($user)->delete("api/v4/companion/{$companionApp->id}");
+ $response->assertStatus(200);
+ $json = $response->json();
+ $this->assertSame('success', $json['status']);
+ $this->assertArrayHasKey('message', $json);
+
+ $companionApp = \App\CompanionApp::where('device_id', 'testdevice')->first();
+ $this->assertTrue($companionApp == null);
+ }
}
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -3,6 +3,7 @@
namespace Tests;
use App\Backends\LDAP;
+use App\CompanionApp;
use App\Domain;
use App\Group;
use App\Resource;
@@ -365,6 +366,24 @@
$user->forceDelete();
}
+ /**
+ * Delete a test companion app whatever it takes.
+ *
+ * @coversNothing
+ */
+ protected function deleteTestCompanionApp($deviceId)
+ {
+ Queue::fake();
+
+ $companionApp = CompanionApp::where('device_id', $deviceId)->first();
+
+ if (!$companionApp) {
+ return;
+ }
+
+ $companionApp->forceDelete();
+ }
+
/**
* Helper to access protected property of an object
*/
@@ -486,6 +505,28 @@
return $user;
}
+ /**
+ * Get CompanionApp object by deviceId, create it if needed.
+ * Skip LDAP jobs.
+ *
+ * @coversNothing
+ */
+ protected function getTestCompanionApp($deviceId, $user, $attrib = [])
+ {
+ // Disable jobs (i.e. skip LDAP oprations)
+ Queue::fake();
+ $companionApp = CompanionApp::firstOrCreate(
+ [
+ 'device_id' => $deviceId,
+ 'user_id' => $user->id,
+ 'notification_token' => '',
+ 'mfa_enabled' => 1
+ ],
+ $attrib
+ );
+ return $companionApp;
+ }
+
/**
* Call protected/private method of a class.
*
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 9:57 AM (10 h, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820361
Default Alt Text
D3029.1775296643.diff (26 KB)
Attached To
Mode
D3029: CompanionApp support
Attached
Detach File
Event Timeline