Complete the Contacts Lists endpoints in the API, with tests and documentation

This commit is contained in:
Timothée Jaussoin 2023-07-06 17:15:20 +02:00
parent c1e355a829
commit c1e3f56e5d
10 changed files with 379 additions and 61 deletions

View file

@ -168,7 +168,7 @@ class AccountController extends Controller
return redirect()->route('admin.account.index'); return redirect()->route('admin.account.index');
} }
public function attachContactsList(Request $request, int $id) public function contactsListAdd(Request $request, int $id)
{ {
$request->validate([ $request->validate([
'contacts_list_id' => 'required|exists:contacts_lists,id' 'contacts_list_id' => 'required|exists:contacts_lists,id'
@ -181,7 +181,7 @@ class AccountController extends Controller
return redirect()->route('admin.account.edit', $id); return redirect()->route('admin.account.edit', $id);
} }
public function detachContactsList(Request $request, int $id) public function contactsListRemove(Request $request, int $id)
{ {
$account = Account::findOrFail($id); $account = Account::findOrFail($id);
$account->contactsLists()->detach([$request->get('contacts_list_id')]); $account->contactsLists()->detach([$request->get('contacts_list_id')]);

View file

@ -29,6 +29,7 @@ use App\Account;
use App\AccountTombstone; use App\AccountTombstone;
use App\AccountType; use App\AccountType;
use App\ActivationExpiration; use App\ActivationExpiration;
use App\ContactsList;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Http\Requests\CreateAccountRequest; use App\Http\Requests\CreateAccountRequest;
use App\Http\Requests\UpdateAccountRequest; use App\Http\Requests\UpdateAccountRequest;
@ -205,6 +206,26 @@ class AccountController extends Controller
return Account::findOrFail($id)->types()->detach($typeId); return Account::findOrFail($id)->types()->detach($typeId);
} }
public function contactsListAdd(int $id, int $contactsListId)
{
if (Account::findOrFail($id)->contactsLists()->pluck('id')->contains($contactsListId)) {
abort(403);
}
if (ContactsList::findOrFail($contactsListId)) {
return Account::findOrFail($id)->contactsLists()->attach($contactsListId);
}
}
public function contactsListRemove(int $id, int $contactsListId)
{
if (!Account::findOrFail($id)->contactsLists()->pluck('id')->contains($contactsListId)) {
abort(403);
}
return Account::findOrFail($id)->contactsLists()->detach($contactsListId);
}
public function recoverByEmail(int $id) public function recoverByEmail(int $id)
{ {
$account = Account::findOrFail($id); $account = Account::findOrFail($id);

View file

@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Account;
use App\ContactsList;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ContactsListController extends Controller
{
public function index(Request $request)
{
return ContactsList::all();
}
public function get(int $contactsListId)
{
return ContactsList::where('id', $contactsListId)
->firstOrFail();
}
public function store(Request $request)
{
$request->validate([
'title' => ['required'],
'description' => ['required']
]);
$contactsList = new ContactsList;
$contactsList->title = $request->get('title');
$contactsList->description = $request->get('description');
$contactsList->save();
return $contactsList;
}
public function update(Request $request, int $contactsListId)
{
$request->validate([
'title' => ['required'],
'description' => ['required']
]);
$contactsList = ContactsList::where('id', $contactsListId)
->firstOrFail();
$contactsList->title = $request->get('title');
$contactsList->description = $request->get('description');
$contactsList->save();
return $contactsList;
}
public function destroy(int $contactsListId)
{
return ContactsList::where('id', $contactsListId)
->delete();
}
public function contactAdd(int $id, int $contactId)
{
if (ContactsList::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) {
abort(403);
}
if (Account::findOrFail($contactId)) {
return ContactsList::findOrFail($id)->contacts()->attach($contactId);
}
}
public function contactRemove(int $id, int $contactId)
{
if (!ContactsList::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) {
abort(403);
}
return ContactsList::findOrFail($id)->contacts()->detach($contactId);
}
}

44
flexiapi/composer.lock generated
View file

@ -399,16 +399,16 @@
}, },
{ {
"name": "doctrine/dbal", "name": "doctrine/dbal",
"version": "3.6.3", "version": "3.6.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/dbal.git", "url": "https://github.com/doctrine/dbal.git",
"reference": "9a747d29e7e6b39509b8f1847e37a23a0163ea6a" "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/9a747d29e7e6b39509b8f1847e37a23a0163ea6a", "url": "https://api.github.com/repos/doctrine/dbal/zipball/19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f",
"reference": "9a747d29e7e6b39509b8f1847e37a23a0163ea6a", "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -491,7 +491,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/dbal/issues", "issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.6.3" "source": "https://github.com/doctrine/dbal/tree/3.6.4"
}, },
"funding": [ "funding": [
{ {
@ -507,7 +507,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-06-01T05:46:46+00:00" "time": "2023-06-15T07:40:12+00:00"
}, },
{ {
"name": "doctrine/deprecations", "name": "doctrine/deprecations",
@ -1729,16 +1729,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v9.52.9", "version": "v9.52.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "c512ece7b1ee393eac5893f37cb2b029a5413b97" "reference": "858add225ce88a76c43aec0e7866288321ee0ee9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/c512ece7b1ee393eac5893f37cb2b029a5413b97", "url": "https://api.github.com/repos/laravel/framework/zipball/858add225ce88a76c43aec0e7866288321ee0ee9",
"reference": "c512ece7b1ee393eac5893f37cb2b029a5413b97", "reference": "858add225ce88a76c43aec0e7866288321ee0ee9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1923,7 +1923,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2023-06-08T20:06:23+00:00" "time": "2023-06-27T13:25:54+00:00"
}, },
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
@ -2615,16 +2615,16 @@
}, },
{ {
"name": "nesbot/carbon", "name": "nesbot/carbon",
"version": "2.67.0", "version": "2.68.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/briannesbitt/Carbon.git", "url": "https://github.com/briannesbitt/Carbon.git",
"reference": "c1001b3bc75039b07f38a79db5237c4c529e04c8" "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/c1001b3bc75039b07f38a79db5237c4c529e04c8", "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da",
"reference": "c1001b3bc75039b07f38a79db5237c4c529e04c8", "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2713,7 +2713,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-05-25T22:09:47+00:00" "time": "2023-06-20T18:29:04+00:00"
}, },
{ {
"name": "nette/schema", "name": "nette/schema",
@ -7484,16 +7484,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v4.15.5", "version": "v4.16.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e" "reference": "19526a33fb561ef417e822e85f08a00db4059c17"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/11e2663a5bc9db5d714eedb4277ee300403b4a9e", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17",
"reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e", "reference": "19526a33fb561ef417e822e85f08a00db4059c17",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7534,9 +7534,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.15.5" "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0"
}, },
"time": "2023-05-19T20:20:00+00:00" "time": "2023-06-25T14:52:30+00:00"
}, },
{ {
"name": "nunomaduro/collision", "name": "nunomaduro/collision",

View file

@ -88,6 +88,14 @@ body {
--danger-7: rgba(158, 53, 72, 1); --danger-7: rgba(158, 53, 72, 1);
--danger-8: rgba(127, 37, 61, 1); --danger-8: rgba(127, 37, 61, 1);
--danger-9: rgba(104, 26, 54, 1); --danger-9: rgba(104, 26, 54, 1);
--color-orange: rgba(254, 94, 0, 1);
--color-grey: rgba(108, 122, 135, 1);
--color-green: rgba(79, 174, 128, 1);
--color-blue: rgba(9, 197, 244, 1);
--color-yellow: rgba(255, 220, 46, 1);
--color-pink: rgba(255, 94, 102, 1);
--color-purple: rgba(151, 71, 255, 1);
} }
body.show_menu { body.show_menu {
@ -468,6 +476,7 @@ h3 {
font-size: 1.75rem; font-size: 1.75rem;
color: var(--second-6); color: var(--second-6);
padding: 0.5rem 0; padding: 0.5rem 0;
margin-top: 1.25rem;
} }
h4 { h4 {
@ -488,6 +497,18 @@ h4 {
padding: 0.25rem 1.25rem; padding: 0.25rem 1.25rem;
} }
.badge.badge-info {
background-color: var(--color-blue);
}
.badge.badge-success {
background-color: var(--color-green);
}
.badge.badge-warning {
background-color: var(--color-yellow);
}
/** Table **/ /** Table **/
table { table {

111
flexiapi/public/scripts/utils.js vendored Normal file
View file

@ -0,0 +1,111 @@
/**
* @brief Set object in localStorage
* @param key string
* @param value the object
*/
Storage.prototype.setObject = function (key, value) {
this.setItem(key, JSON.stringify(value));
};
/**
* @brief Get object in localStorage
* @param key
*/
Storage.prototype.getObject = function (key) {
return JSON.parse(this.getItem(key));
};
var Utils = {
toggleAll: function (checkbox) {
checkbox.closest('table').querySelectorAll('tbody input[type=checkbox]').forEach(element => {
element.checked = checkbox.checked;
element.dispatchEvent(new Event('change'));
});
},
getStorageList: function (key) {
var list = sessionStorage.getObject('list.' + key);
if (list == null) {
list = [];
}
return list;
},
addToStorageList: function (key, id) {
var list = Utils.getStorageList(key);
if (!list.includes(id)) {
list.push(id);
}
sessionStorage.setObject('list.' + key, list);
},
removeFromStorageList: function(key, id) {
var list = Utils.getStorageList(key);
list.splice(list.indexOf(id), 1);
sessionStorage.setObject('list.' + key, list);
},
existsInStorageList: function(key, id) {
var list = Utils.getStorageList(key);
return (list && list.includes(id));
},
clearStorageList: function (key) {
sessionStorage.setObject('list.' + key, []);
},
/** List toggle */
}
var ListToggle = {
init: function() {
document.querySelectorAll('input[type=checkbox].list_toggle').forEach(checkbox => {
checkbox.checked = Utils.existsInStorageList(checkbox.dataset.listId, checkbox.dataset.id);
checkbox.addEventListener('change', e => {
if (checkbox.checked) {
Utils.addToStorageList(checkbox.dataset.listId, checkbox.dataset.id);
} else {
Utils.removeFromStorageList(checkbox.dataset.listId, checkbox.dataset.id);
}
ListToggle.refreshFormList();
ListToggle.refreshCounters();
})
});
ListToggle.refreshFormList();
ListToggle.refreshCounters();
},
refreshFormList: function() {
document.querySelectorAll('select.list_toggle').forEach(select => {
select.innerHTML = '';
select.multiple = true;
Utils.getStorageList(select.dataset.listId).forEach(id => {
const option = document.createElement("option");
option.value = id;
option.text = id;
option.selected = true;
select.add(option, null);
});
});
},
refreshCounters: function() {
document.querySelectorAll('span.list_toggle').forEach(counter => {
counter.innerHTML = Utils.getStorageList(counter.dataset.listId).length;
});
}
}
document.addEventListener("DOMContentLoaded", function(event) {
ListToggle.init();
});

View file

@ -428,6 +428,46 @@ JSON parameters:
<span class="badge badge-warning">Admin</span> <span class="badge badge-warning">Admin</span>
Delete an account related action. Delete an account related action.
## Contacts Lists
### `GET /contacts_lists`
<span class="badge badge-warning">Admin</span>
Show all the contacts lists.
### `GET /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Show a contacts list.
### `POST /contacts_lists`
<span class="badge badge-warning">Admin</span>
Create a contacts list.
JSON parameters:
* `title` required
* `description` required
### `PUT /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Update a contacts list.
JSON parameters:
* `title` required
* `description` required
### `DELETE /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Delete a contacts list.
### `POST /accounts/{id}/contacts_lists/{contacts_list_id}`
<span class="badge badge-warning">Admin</span>
Add a contacts list to the account.
### `DELETE /accounts/{id}/contacts_lists/{contacts_list_id}`
<span class="badge badge-warning">Admin</span>
Remove a contacts list from the account.
## Account Types ## Account Types
### `GET /account_types` ### `GET /account_types`
@ -464,7 +504,7 @@ Add a type to the account.
### `DELETE /accounts/{id}/contacts/{type_id}` ### `DELETE /accounts/{id}/contacts/{type_id}`
<span class="badge badge-warning">Admin</span> <span class="badge badge-warning">Admin</span>
Remove a a type from the account. Remove a type from the account.
## Messages ## Messages

View file

@ -17,6 +17,11 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
use App\Http\Controllers\Api\Admin\AccountActionController;
use App\Http\Controllers\Api\Admin\AccountContactController;
use App\Http\Controllers\Api\Admin\AccountController as AdminAccountController;
use App\Http\Controllers\Api\Admin\AccountTypeController;
use App\Http\Controllers\Api\Admin\ContactsListController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
Route::get('/', 'Api\ApiController@documentation')->name('api'); Route::get('/', 'Api\ApiController@documentation')->name('api');
@ -81,41 +86,43 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () {
Route::post('account_creation_tokens', 'Api\Admin\AccountCreationTokenController@create'); Route::post('account_creation_tokens', 'Api\Admin\AccountCreationTokenController@create');
// Accounts // Accounts
Route::get('accounts/{id}/activate', 'Api\Admin\AccountController@activate'); Route::prefix('accounts')->controller(AdminAccountController::class)->group(function () {
Route::get('accounts/{id}/deactivate', 'Api\Admin\AccountController@deactivate'); Route::get('{id}/activate', 'activate');
Route::get('accounts/{id}/provision', 'Api\Admin\AccountController@provision'); Route::get('{id}/deactivate', 'deactivate');
Route::get('{id}/provision', 'provision');
Route::post('accounts/{id}/recover-by-email', 'Api\Admin\AccountController@recoverByEmail'); Route::post('{id}/recover-by-email', 'recoverByEmail');
Route::post('accounts', 'Api\Admin\AccountController@store'); Route::post('/', 'store');
Route::put('accounts/{id}', 'Api\Admin\AccountController@update'); Route::put('{id}', 'update');
Route::get('accounts', 'Api\Admin\AccountController@index'); Route::get('/', 'index');
Route::get('accounts/{id}', 'Api\Admin\AccountController@show'); Route::get('{id}', 'show');
Route::delete('accounts/{id}', 'Api\Admin\AccountController@destroy'); Route::delete('{id}', 'destroy');
Route::get('accounts/{sip}/search', 'Api\Admin\AccountController@search'); Route::get('{sip}/search', 'search');
Route::get('accounts/{email}/search-by-email', 'Api\Admin\AccountController@searchByEmail'); Route::get('{email}/search-by-email', 'searchByEmail');
// Account actions Route::post('{id}/types/{type_id}', 'typeAdd');
Route::get('accounts/{id}/actions', 'Api\Admin\AccountActionController@index'); Route::delete('{id}/types/{type_id}', 'typeRemove');
Route::get('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@show');
Route::post('accounts/{id}/actions', 'Api\Admin\AccountActionController@store'); Route::post('{id}/contacts_lists/{contacts_list_id}', 'contactsListAdd');
Route::delete('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@destroy'); Route::delete('{id}/contacts_lists/{contacts_list_id}', 'contactsListRemove');
Route::put('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@update'); });
// Account contacts // Account contacts
Route::get('accounts/{id}/contacts', 'Api\Admin\AccountContactController@index'); Route::prefix('accounts/{id}/contacts')->controller(AccountContactController::class)->group(function () {
Route::get('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@show'); Route::get('/', 'index');
Route::post('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@add'); Route::get('{contact_id}', 'show');
Route::delete('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@remove'); Route::post('{contact_id}', 'add');
Route::delete('{contact_id}', 'remove');
});
// Account types Route::apiResource('accounts/{id}/actions', AccountActionController::class);
Route::get('account_types', 'Api\Admin\AccountTypeController@index'); Route::apiResource('account_types', AccountTypeController::class);
Route::get('account_types/{id}', 'Api\Admin\AccountTypeController@show');
Route::post('account_types', 'Api\Admin\AccountTypeController@store');
Route::delete('account_types/{id}', 'Api\Admin\AccountTypeController@destroy');
Route::put('account_types/{id}', 'Api\Admin\AccountTypeController@update');
Route::post('accounts/{id}/types/{type_id}', 'Api\Admin\AccountController@typeAdd'); Route::apiResource('contacts_lists', ContactsListController::class);
Route::delete('accounts/{id}/types/{type_id}', 'Api\Admin\AccountController@typeRemove'); Route::prefix('contacts_lists')->controller(ContactsListController::class)->group(function () {
Route::post('{id}/contacts/{contacts_id}', 'contactAdd');
Route::delete('{id}/contacts/{contacts_id}', 'contactRemove');
});
}); });
}); });

View file

@ -129,7 +129,6 @@ if (config('app.web_panel')) {
Route::get('auth_tokens/qrcode/{token}', 'Account\AuthTokenController@qrcode')->name('auth_tokens.qrcode'); Route::get('auth_tokens/qrcode/{token}', 'Account\AuthTokenController@qrcode')->name('auth_tokens.qrcode');
Route::get('auth_tokens/auth/{token}', 'Account\AuthTokenController@auth')->name('auth_tokens.auth'); Route::get('auth_tokens/auth/{token}', 'Account\AuthTokenController@auth')->name('auth_tokens.auth');
//Route::get('admin/accounts/{acc}/cl/{$cl}/detach', 'Admin\AccountController@detachContactsList')->name('admin.account.contacts_lists.detach2');
Route::name('admin.')->prefix('admin')->middleware(['auth.admin'])->group(function () { Route::name('admin.')->prefix('admin')->middleware(['auth.admin'])->group(function () {
// Statistics // Statistics
@ -155,8 +154,8 @@ if (config('app.web_panel')) {
Route::get('/', 'index')->name('index'); Route::get('/', 'index')->name('index');
Route::post('search', 'search')->name('search'); Route::post('search', 'search')->name('search');
Route::get('{account_id}/contacts_lists/detach', 'detachContactsList')->name('contacts_lists.detach'); Route::get('{account_id}/contacts_lists/detach', 'contactsListRemove')->name('contacts_lists.detach');
Route::post('{account_id}/contacts_lists', 'attachContactsList')->name('contacts_lists.attach'); Route::post('{account_id}/contacts_lists', 'contactsListAdd')->name('contacts_lists.attach');
}); });
Route::name('type.')->prefix('types')->controller(AccountTypeController::class)->group(function () { Route::name('type.')->prefix('types')->controller(AccountTypeController::class)->group(function () {

View file

@ -32,6 +32,7 @@ class ApiAccountContactTest extends TestCase
use RefreshDatabase; use RefreshDatabase;
protected $route = '/api/accounts'; protected $route = '/api/accounts';
protected $contactsListsRoute = '/api/contacts_lists';
protected $method = 'POST'; protected $method = 'POST';
public function testCreate() public function testCreate()
@ -155,11 +156,50 @@ class ApiAccountContactTest extends TestCase
* *
*/ */
// This will need to be done through the API // Create the Contacts list
$contactList = ContactsList::factory()->create(); $contactsListsTitle = 'Contacts List title';
$contactList->contacts()->attach([$password1->account->id, $password2->account->id, $password3->account->id]);
$admin->account->contactsLists()->attach([$contactList->id]); $this->keyAuthenticated($admin->account)
->json($this->method, $this->contactsListsRoute, [
'title' => $contactsListsTitle,
'description' => 'Description'
])
->assertStatus(201);
$this->assertDatabaseHas('contacts_lists', [
'title' => $contactsListsTitle
]);
// Attach the Contacts and the Contacts List
$contactsList = ContactsList::first();
$this->keyAuthenticated($admin->account)
->post($this->contactsListsRoute . '/' . $contactsList->id . '/contacts/' . $password1->account->id)
->assertStatus(200);
$this->keyAuthenticated($admin->account)
->post($this->contactsListsRoute . '/' . $contactsList->id . '/contacts/' . $password2->account->id)
->assertStatus(200);
$this->keyAuthenticated($admin->account)
->post($this->contactsListsRoute . '/' . $contactsList->id . '/contacts/' . $password3->account->id)
->assertStatus(200);
$this->keyAuthenticated($admin->account)
->post($this->contactsListsRoute . '/' . $contactsList->id . '/contacts/1234')
->assertStatus(404);
$this->keyAuthenticated($admin->account)
->post($this->route . '/' . $admin->account->id . '/contacts_lists/' . $contactsList->id)
->assertStatus(200);
$this->keyAuthenticated($admin->account)
->post($this->route . '/' . $admin->account->id . '/contacts_lists/' . $contactsList->id)
->assertStatus(403);
// Get the contacts and vcards
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route . '/me/contacts') ->get($this->route . '/me/contacts')