Page MenuHomePhorge

D3584.1775186142.diff
No OneTemporary

Authored By
Unknown
Size
191 KB
Referenced Files
None
Subscribers
None

D3584.1775186142.diff

diff --git a/src/app/Console/Commands/Meet/RoomCreate.php b/src/app/Console/Commands/Meet/RoomCreate.php
--- a/src/app/Console/Commands/Meet/RoomCreate.php
+++ b/src/app/Console/Commands/Meet/RoomCreate.php
@@ -47,9 +47,10 @@
return 1;
}
- \App\Meet\Room::create([
+ $room = \App\Meet\Room::create([
'name' => $roomName,
- 'user_id' => $user->id
]);
+
+ $room->assignToWallet($user->wallets()->first());
}
}
diff --git a/src/app/Console/Commands/Meet/Sessions.php b/src/app/Console/Commands/Meet/Sessions.php
--- a/src/app/Console/Commands/Meet/Sessions.php
+++ b/src/app/Console/Commands/Meet/Sessions.php
@@ -49,7 +49,7 @@
foreach ($sessions as $session) {
$room = \App\Meet\Room::where('session_id', $session['roomId'])->first();
if ($room) {
- $owner = $room->owner->email;
+ $owner = $room->wallet()->owner->email;
$roomName = $room->name;
} else {
$owner = '(none)';
diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -326,6 +326,16 @@
);
}
+ /**
+ * Returns domain's namespace (required by the EntitleableTrait).
+ *
+ * @return string|null Domain namespace
+ */
+ public function toString(): ?string
+ {
+ return $this->namespace;
+ }
+
/**
* Unsuspend this domain.
*
diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php
--- a/src/app/Entitlement.php
+++ b/src/app/Entitlement.php
@@ -100,20 +100,6 @@
return $this->morphTo()->withTrashed(); // @phpstan-ignore-line
}
- /**
- * Returns entitleable object title (e.g. email or domain name).
- *
- * @return string|null An object title/name
- */
- public function entitleableTitle(): ?string
- {
- if ($this->entitleable instanceof Domain) {
- return $this->entitleable->namespace;
- }
-
- return $this->entitleable->email;
- }
-
/**
* Simplified Entitlement/SKU information for a specified entitleable object
*
diff --git a/src/app/Handlers/Base.php b/src/app/Handlers/Base.php
--- a/src/app/Handlers/Base.php
+++ b/src/app/Handlers/Base.php
@@ -19,8 +19,8 @@
* to the user/domain when either it is active or there's already an
* active entitlement.
*
- * @param \App\Sku $sku The SKU object
- * @param \App\User|\App\Domain $object The user or domain object
+ * @param \App\Sku $sku The SKU
+ * @param object $object The entitleable object
*
* @return bool
*/
diff --git a/src/app/Handlers/Meet.php b/src/app/Handlers/GroupRoom.php
rename from src/app/Handlers/Meet.php
rename to src/app/Handlers/GroupRoom.php
--- a/src/app/Handlers/Meet.php
+++ b/src/app/Handlers/GroupRoom.php
@@ -2,7 +2,7 @@
namespace App\Handlers;
-class Meet extends Base
+class GroupRoom extends Base
{
/**
* The entitleable class for this handler.
@@ -11,7 +11,7 @@
*/
public static function entitleableClass(): string
{
- return \App\User::class;
+ return \App\Meet\Room::class;
}
/**
@@ -25,19 +25,9 @@
{
$data = parent::metadata($sku);
- $data['required'] = ['Groupware'];
+ $data['exclusive'] = ['Room'];
+ $data['controllerOnly'] = true;
return $data;
}
-
- /**
- * The priority that specifies the order of SKUs in UI.
- * Higher number means higher on the list.
- *
- * @return int
- */
- public static function priority(): int
- {
- return 50;
- }
}
diff --git a/src/app/Handlers/Meet.php b/src/app/Handlers/Room.php
rename from src/app/Handlers/Meet.php
rename to src/app/Handlers/Room.php
--- a/src/app/Handlers/Meet.php
+++ b/src/app/Handlers/Room.php
@@ -2,7 +2,7 @@
namespace App\Handlers;
-class Meet extends Base
+class Room extends Base
{
/**
* The entitleable class for this handler.
@@ -11,7 +11,7 @@
*/
public static function entitleableClass(): string
{
- return \App\User::class;
+ return \App\Meet\Room::class;
}
/**
@@ -25,7 +25,8 @@
{
$data = parent::metadata($sku);
- $data['required'] = ['Groupware'];
+ $data['enabled'] = true;
+ $data['exclusive'] = ['GroupRoom'];
return $data;
}
@@ -38,6 +39,6 @@
*/
public static function priority(): int
{
- return 50;
+ return 10;
}
}
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -165,7 +165,7 @@
return response()->json([
'status' => 'success',
- 'message' => __('app.domain-create-success'),
+ 'message' => \trans('app.domain-create-success'),
]);
}
diff --git a/src/app/Http/Controllers/API/V4/MeetController.php b/src/app/Http/Controllers/API/V4/MeetController.php
--- a/src/app/Http/Controllers/API/V4/MeetController.php
+++ b/src/app/Http/Controllers/API/V4/MeetController.php
@@ -6,46 +6,9 @@
use App\Meet\Room;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Validator;
class MeetController extends Controller
{
- /**
- * Listing of rooms that belong to the authenticated user.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function index()
- {
- $user = Auth::guard()->user();
-
- $rooms = Room::where('user_id', $user->id)->orderBy('name')->get();
-
- if (count($rooms) == 0) {
- // Create a room for the user (with a random and unique name)
- while (true) {
- $name = strtolower(\App\Utils::randStr(3, 3, '-'));
- if (!Room::where('name', $name)->count()) {
- break;
- }
- }
-
- $room = Room::create([
- 'name' => $name,
- 'user_id' => $user->id
- ]);
-
- $rooms = collect([$room]);
- }
-
- $result = [
- 'list' => $rooms,
- 'count' => count($rooms),
- ];
-
- return response()->json($result);
- }
-
/**
* Join the room session. Each room has one owner, and the room isn't open until the owner
* joins (and effectively creates the session).
@@ -59,17 +22,14 @@
$room = Room::where('name', $id)->first();
// Room does not exist, or the owner is deleted
- if (!$room || !$room->owner || $room->owner->isDegraded(true)) {
- return $this->errorResponse(404, \trans('meet.room-not-found'));
- }
-
- // Check if there's still a valid meet entitlement for the room owner
- if (!$room->owner->hasSku('meet')) {
+ if (!$room || !($wallet = $room->wallet()) || !$wallet->owner || $wallet->owner->isDegraded(true)) {
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
$user = Auth::guard()->user();
- $isOwner = $user && $user->id == $room->user_id;
+ $isOwner = $user && (
+ $user->id == $wallet->owner->id || $room->permissions()->where('user', $user->email)->exists()
+ );
$init = !empty(request()->input('init'));
// There's no existing session
@@ -180,69 +140,6 @@
return response()->json($response, $response_code);
}
- /**
- * Set the domain configuration.
- *
- * @param string $id Room identifier (name)
- *
- * @return \Illuminate\Http\JsonResponse|void
- */
- public function setRoomConfig($id)
- {
- $room = Room::where('name', $id)->first();
-
- // Room does not exist, or the owner is deleted
- if (!$room || !$room->owner || $room->owner->isDegraded(true)) {
- return $this->errorResponse(404);
- }
-
- $user = Auth::guard()->user();
-
- // Only room owner can configure the room
- if ($user->id != $room->user_id) {
- return $this->errorResponse(403);
- }
-
- $input = request()->input();
- $errors = [];
-
- foreach ($input as $key => $value) {
- switch ($key) {
- case 'password':
- if ($value === null || $value === '') {
- $input[$key] = null;
- } else {
- // TODO: Do we have to validate the password in any way?
- }
- break;
-
- case 'locked':
- $input[$key] = $value ? 'true' : null;
- break;
-
- case 'nomedia':
- $input[$key] = $value ? 'true' : null;
- break;
-
- default:
- $errors[$key] = \trans('meet.room-unsupported-option-error');
- }
- }
-
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
- }
-
- if (!empty($input)) {
- $room->setSettings($input);
- }
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('meet.room-setconfig-success'),
- ]);
- }
-
/**
* Webhook as triggered from the Meet server
*
diff --git a/src/app/Http/Controllers/API/V4/RoomsController.php b/src/app/Http/Controllers/API/V4/RoomsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/RoomsController.php
@@ -0,0 +1,314 @@
+<?php
+
+namespace App\Http\Controllers\API\V4;
+
+use App\Http\Controllers\RelationController;
+use App\Meet\Room;
+use App\Permission;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Validator;
+
+class RoomsController extends RelationController
+{
+ /** @var string Resource localization label */
+ protected $label = 'room';
+
+ /** @var string Resource model name */
+ protected $model = Room::class;
+
+ /** @var array Resource listing order (column names) */
+ protected $order = ['name'];
+
+ /** @var array Common object properties in the API response */
+ protected $objectProps = ['name', 'description'];
+
+
+ /**
+ * Delete a room
+ *
+ * @param string $id Room identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function destroy($id)
+ {
+ $room = $this->inputRoom($id);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ $room->delete();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.room-delete-success"),
+ ]);
+ }
+
+ /**
+ * Listing of rooms that belong to the authenticated user.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $user = $this->guard()->user();
+
+ $shared = Room::whereIn('id', function ($query) use ($user) {
+ $query->select('permissible_id')
+ ->from('permissions')
+ ->where('permissible_type', Room::class)
+ ->where('user', $user->email);
+ });
+
+ // Create a "private" room for the user
+ if (!$user->rooms()->count()) {
+ $room = Room::create();
+ $room->assignToWallet($user->wallets()->first());
+ }
+
+ $rooms = $user->rooms(true)->union($shared)->orderBy('name')->get()
+ ->map(function ($room) {
+ return $this->objectToClient($room);
+ });
+
+ $result = [
+ 'list' => $rooms,
+ 'count' => count($rooms),
+ ];
+
+ return response()->json($result);
+ }
+
+ /**
+ * Set the room configuration.
+ *
+ * @param int|string $id Room identifier (or name)
+ *
+ * @return \Illuminate\Http\JsonResponse|void
+ */
+ public function setConfig($id)
+ {
+ $room = $this->inputRoom($id, Permission::ADMIN, $permission);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ $request = request()->input();
+
+ // Room sharees can't manage room ACL
+ if ($permission) {
+ unset($request['acl']);
+ }
+
+ $errors = $room->setConfig($request);
+
+ if (!empty($errors)) {
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.room-setconfig-success"),
+ ]);
+ }
+
+ /**
+ * Display information of a room specified by $id.
+ *
+ * @param string $id The room to show information for.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function show($id)
+ {
+ $room = $this->inputRoom($id, Permission::READ, $permission);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ $wallet = $room->wallet();
+ $user = $this->guard()->user();
+
+ $response = $this->objectToClient($room, true);
+
+ unset($response['session_id']);
+
+ $response['config'] = $room->getConfig();
+
+ // Room sharees can't manage/see room ACL
+ if ($permission) {
+ unset($response['config']['acl']);
+ }
+
+ $response['skus'] = \App\Entitlement::objectEntitlementsSummary($room);
+ $response['wallet'] = $wallet->toArray();
+
+ if ($wallet->discount) {
+ $response['wallet']['discount'] = $wallet->discount->discount;
+ $response['wallet']['discount_description'] = $wallet->discount->description;
+ }
+
+ $isOwner = $user->canDelete($room);
+ $response['canUpdate'] = $isOwner || $room->permissions()->where('user', $user->email)->exists();
+ $response['canDelete'] = $isOwner && $user->wallet()->isController($user);
+ $response['canShare'] = $isOwner && $room->hasSKU('group-room');
+ $response['isOwner'] = $isOwner;
+
+ return response()->json($response);
+ }
+
+ /**
+ * Get a list of SKUs available to the room.
+ *
+ * @param int $id Room identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function skus($id)
+ {
+ $room = $this->inputRoom($id);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ return SkusController::objectSkus($room);
+ }
+
+ /**
+ * Create a new room.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function store(Request $request)
+ {
+ $user = $this->guard()->user();
+ $wallet = $user->wallet();
+
+ if (!$wallet->isController($user)) {
+ return $this->errorResponse(403);
+ }
+
+ // Validate the input
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'description' => 'nullable|string|max:191'
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ DB::beginTransaction();
+
+ $room = Room::create([
+ 'description' => $request->input('description'),
+ ]);
+
+ if (!empty($request->skus)) {
+ SkusController::updateEntitlements($room, $request->skus, $wallet);
+ } else {
+ $room->assignToWallet($wallet);
+ }
+
+ DB::commit();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.room-create-success"),
+ ]);
+ }
+
+ /**
+ * Update a room.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ * @param string $id Room identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function update(Request $request, $id)
+ {
+ $room = $this->inputRoom($id, Permission::ADMIN);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ // Validate the input
+ $v = Validator::make(
+ request()->all(),
+ [
+ 'description' => 'nullable|string|max:191'
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ DB::beginTransaction();
+
+ $room->description = request()->input('description');
+ $room->save();
+
+ if (!empty($request->skus)) {
+ SkusController::updateEntitlements($room, $request->skus);
+ }
+
+ if (!$room->hasSKU('group-room')) {
+ $room->setSetting('acl', null);
+ }
+
+ DB::commit();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.room-update-success"),
+ ]);
+ }
+
+ /**
+ * Get the input room object, check permissions.
+ *
+ * @param int|string $id Room identifier (or name)
+ * @param ?int $rights Required access rights
+ * @param ?\App\Permission $permission Room permission reference if the user has permissions
+ * to the room and is not the owner
+ *
+ * @return \App\Meet\Room|int File object or error code
+ */
+ protected function inputRoom($id, $rights = 0, &$permission = null): int|Room
+ {
+ if (!is_numeric($id)) {
+ $room = Room::where('name', $id)->first();
+ } else {
+ $room = Room::find($id);
+ }
+
+ if (!$room) {
+ return 404;
+ }
+
+ $user = $this->guard()->user();
+
+ // Room owner (or another wallet controller)?
+ if ($room->wallet()->isController($user)) {
+ return $room;
+ }
+
+ if ($rights) {
+ $permission = $room->permissions()->where('user', $user->email)->first();
+
+ if ($permission && $permission->rights & $rights) {
+ return $room;
+ }
+ }
+
+ return 403;
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -5,6 +5,8 @@
use App\Http\Controllers\ResourceController;
use App\Sku;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Str;
class SkusController extends ResourceController
{
@@ -51,15 +53,14 @@
}
/**
- * Return SKUs available to the specified user/domain.
+ * Return SKUs available to the specified entitleable object.
*
- * @param object $object User or Domain object
+ * @param object $object Entitleable object
*
* @return \Illuminate\Http\JsonResponse
*/
public static function objectSkus($object)
{
- $type = \lcfirst(\class_basename($object::class));
$response = [];
// Note: Order by title for consistent ordering in tests
@@ -70,13 +71,20 @@
continue;
}
+ if ($object::class != $sku->handler_class::entitleableClass()) {
+ continue;
+ }
+
if (!$sku->handler_class::isAvailable($sku, $object)) {
continue;
}
if ($data = self::skuElement($sku)) {
- if ($type != $data['type']) {
- continue;
+ if (!empty($data['controllerOnly'])) {
+ $user = Auth::guard()->user();
+ if (!$user->wallet()->isController($user)) {
+ continue;
+ }
}
$response[] = $data;
@@ -113,10 +121,11 @@
/**
* Update object entitlements.
*
- * @param object $object The object for update
- * @param array $rSkus List of SKU IDs requested for the user in the form [id=>qty]
+ * @param object $object The object for update
+ * @param array $rSkus List of SKU IDs requested for the object in the form [id=>qty]
+ * @param ?\App\Wallet $wallet The target wallet
*/
- public static function updateEntitlements($object, $rSkus): void
+ public static function updateEntitlements($object, $rSkus, $wallet = null): void
{
if (!is_array($rSkus)) {
return;
@@ -143,6 +152,10 @@
$e = array_key_exists($skuID, $eSkus) ? $eSkus[$skuID] : 0;
$r = array_key_exists($skuID, $rSkus) ? $rSkus[$skuID] : 0;
+ if (!is_a($object, $sku->handler_class::entitleableClass())) {
+ continue;
+ }
+
if ($sku->handler_class == \App\Handlers\Mailbox::class) {
if ($r != 1) {
throw new \Exception("Invalid quantity of mailboxes");
@@ -154,7 +167,7 @@
$object->removeSku($sku, ($e - $r));
} elseif ($e < $r) {
// add those requested more than entitled
- $object->assignSku($sku, ($r - $e));
+ $object->assignSku($sku, ($r - $e), $wallet);
}
}
}
diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php
--- a/src/app/Http/Controllers/RelationController.php
+++ b/src/app/Http/Controllers/RelationController.php
@@ -125,6 +125,8 @@
{
if ($full) {
$result = $object->toArray();
+
+ unset($result['tenant_id']);
} else {
$result = ['id' => $object->id];
diff --git a/src/app/Meet/Room.php b/src/app/Meet/Room.php
--- a/src/app/Meet/Room.php
+++ b/src/app/Meet/Room.php
@@ -2,21 +2,34 @@
namespace App\Meet;
+use App\Traits\BelongsToTenantTrait;
+use App\Traits\EntitleableTrait;
+use App\Traits\Meet\RoomConfigTrait;
+use App\Traits\PermissibleTrait;
use App\Traits\SettingsTrait;
+use Dyrynda\Database\Support\NullableFields;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
/**
* The eloquent definition of a Room.
*
- * @property int $id Room identifier
- * @property string $name Room name
- * @property int $user_id Room owner
- * @property ?string $session_id Meet session identifier
+ * @property int $id Room identifier
+ * @property ?string $description Description
+ * @property string $name Room name
+ * @property int $tenant_id Tenant identifier
+ * @property ?string $session_id Meet session identifier
*/
class Room extends Model
{
+ use BelongsToTenantTrait;
+ use EntitleableTrait;
+ use RoomConfigTrait;
+ use NullableFields;
use SettingsTrait;
+ use PermissibleTrait;
+ use SoftDeletes;
public const ROLE_SUBSCRIBER = 1 << 0;
public const ROLE_PUBLISHER = 1 << 1;
@@ -27,8 +40,18 @@
public const REQUEST_ACCEPTED = 'accepted';
public const REQUEST_DENIED = 'denied';
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'created_at' => 'datetime:Y-m-d H:i:s',
+ 'deleted_at' => 'datetime:Y-m-d H:i:s',
+ 'updated_at' => 'datetime:Y-m-d H:i:s',
+ ];
+
/** @var array<int, string> The attributes that are mass assignable */
- protected $fillable = ['user_id', 'name'];
+ protected $fillable = ['name', 'description'];
+
+ /** @var array<int, string> The attributes that can be not set */
+ protected $nullable = ['description'];
/** @var string Database table name */
protected $table = 'openvidu_rooms';
@@ -180,16 +203,6 @@
return $response->getStatusCode() == 200;
}
- /**
- * The room owner.
- *
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function owner()
- {
- return $this->belongsTo('\App\User', 'user_id', 'id');
- }
-
/**
* Accept the join request.
*
@@ -259,16 +272,6 @@
return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1));
}
- /**
- * Any (additional) properties of this room.
- *
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
- */
- public function settings()
- {
- return $this->hasMany('App\Meet\RoomSetting', 'room_id');
- }
-
/**
* Send a signal to the Meet session participants (peers)
*
@@ -299,6 +302,28 @@
return $response->getStatusCode() == 200;
}
+ /**
+ * Returns a map of supported ACL labels.
+ *
+ * @return array Map of supported permission rights/ACL labels
+ */
+ protected function supportedACL(): array
+ {
+ return [
+ 'full' => \App\Permission::READ | \App\Permission::WRITE | \App\Permission::ADMIN,
+ ];
+ }
+
+ /**
+ * Returns room name (required by the EntitleableTrait).
+ *
+ * @return string|null Room name
+ */
+ public function toString(): ?string
+ {
+ return $this->name;
+ }
+
/**
* Log an error for a failed request to the meet server
*
diff --git a/src/app/Meet/RoomSetting.php b/src/app/Meet/RoomSetting.php
--- a/src/app/Meet/RoomSetting.php
+++ b/src/app/Meet/RoomSetting.php
@@ -14,10 +14,10 @@
*/
class RoomSetting extends Model
{
- protected $fillable = [
- 'room_id', 'key', 'value'
- ];
+ /** @var array<int, string> The attributes that are mass assignable */
+ protected $fillable = ['room_id', 'key', 'value'];
+ /** @var string Database table name */
protected $table = 'openvidu_room_settings';
/**
@@ -27,6 +27,6 @@
*/
public function room()
{
- return $this->belongsTo('\App\Meet\Room', 'room_id', 'id');
+ return $this->belongsTo(Room::class);
}
}
diff --git a/src/app/Observers/Meet/RoomObserver.php b/src/app/Observers/Meet/RoomObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/Meet/RoomObserver.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Observers\Meet;
+
+use App\Meet\Room;
+
+class RoomObserver
+{
+ /**
+ * Handle the room "created" event.
+ *
+ * @param \App\Meet\Room $room The room
+ *
+ * @return void
+ */
+ public function creating(Room $room): void
+ {
+ if (empty($room->name)) {
+ // Generate a random and unique room name
+ while (true) {
+ $room->name = strtolower(\App\Utils::randStr(3, 3, '-'));
+ if (!Room::where('name', $room->name)->exists()) {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -95,6 +95,9 @@
}
});
}
+
+ // TODO: Remove Permission records for the user
+ // TODO: Remove file permissions for the user
}
/**
@@ -263,6 +266,10 @@
return;
}
+ if (!$entitlement->entitleable) {
+ return;
+ }
+
// Objects need to be deleted one by one to make sure observers can do the proper cleanup
if ($force) {
$entitlement->entitleable->forceDelete();
diff --git a/src/app/Permission.php b/src/app/Permission.php
new file mode 100644
--- /dev/null
+++ b/src/app/Permission.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace App;
+
+use App\Traits\UuidStrKeyTrait;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of a Permission.
+ *
+ * @property string $id Permission identifier
+ * @property int $rights Access rights
+ * @property int $permissible_id The shared object identifier
+ * @property string $permissible_type The shared object type (class name)
+ * @property string $user Permitted user (email)
+ */
+class Permission extends Model
+{
+ use UuidStrKeyTrait;
+
+ public const READ = 1;
+ public const WRITE = 2;
+ public const ADMIN = 4;
+
+
+ /** @var array<int, string> The attributes that are mass assignable */
+ protected $fillable = [
+ 'permissible_id',
+ 'permissible_type',
+ 'rights',
+ 'user',
+ ];
+
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'rights' => 'integer',
+ ];
+
+ /**
+ * Principally permissible object such as Room.
+ * Note that it may be trashed (soft-deleted).
+ *
+ * @return mixed
+ */
+ public function permissible()
+ {
+ return $this->morphTo()->withTrashed(); // @phpstan-ignore-line
+ }
+
+ /**
+ * Rights mutator. Make sure rights is integer.
+ */
+ public function setRightsAttribute($rights): void
+ {
+ $this->attributes['rights'] = (int) $rights;
+ }
+}
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -48,6 +48,7 @@
\App\Entitlement::observe(\App\Observers\EntitlementObserver::class);
\App\Group::observe(\App\Observers\GroupObserver::class);
\App\GroupSetting::observe(\App\Observers\GroupSettingObserver::class);
+ \App\Meet\Room::observe(\App\Observers\Meet\RoomObserver::class);
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\PlanPackage::observe(\App\Observers\PlanPackageObserver::class);
\App\Resource::observe(\App\Observers\ResourceObserver::class);
diff --git a/src/app/Traits/EntitleableTrait.php b/src/app/Traits/EntitleableTrait.php
--- a/src/app/Traits/EntitleableTrait.php
+++ b/src/app/Traits/EntitleableTrait.php
@@ -39,21 +39,26 @@
}
/**
- * Assign a Sku to an entitleable object.
+ * Assign a SKU to an entitleable object.
*
- * @param \App\Sku $sku The sku to assign.
- * @param int $count Count of entitlements to add
+ * @param \App\Sku $sku The sku to assign.
+ * @param int $count Count of entitlements to add
+ * @param ?\App\Wallet $wallet The wallet to use when objects's wallet is unknown
*
* @return $this
* @throws \Exception
*/
- public function assignSku(Sku $sku, int $count = 1)
+ public function assignSku(Sku $sku, int $count = 1, $wallet = null)
{
- // TODO: I guess wallet could be parametrized in future
- $wallet = $this->wallet();
- $exists = $this->entitlements()->where('sku_id', $sku->id)->count();
+ if (!$wallet) {
+ $wallet = $this->wallet();
+ }
+
+ if (!$wallet) {
+ throw new \Exception("No wallet specified for the new entitlement");
+ }
- // TODO: Make sure the SKU can be assigned to the object
+ $exists = $this->entitlements()->where('sku_id', $sku->id)->count();
while ($count > 0) {
Entitlement::create([
@@ -76,11 +81,12 @@
* Assign the object to a wallet.
*
* @param \App\Wallet $wallet The wallet
+ * @param ?string $title Optional SKU title
*
* @return $this
* @throws \Exception
*/
- public function assignToWallet(Wallet $wallet)
+ public function assignToWallet(Wallet $wallet, $title = null)
{
if (empty($this->id)) {
throw new \Exception("Object not yet exists");
@@ -92,9 +98,11 @@
// Find the SKU title, e.g. \App\SharedFolder -> shared-folder
// Note: it does not work with User/Domain model (yet)
- $title = Str::kebab(\class_basename(self::class));
+ if (!$title) {
+ $title = Str::kebab(\class_basename(self::class));
+ }
- $sku = Sku::withObjectTenantContext($this)->where('title', $title)->first();
+ $sku = $this->skuByTitle($title);
$exists = $wallet->entitlements()->where('sku_id', $sku->id)->count();
Entitlement::create([
@@ -168,7 +176,7 @@
*/
public function hasSku(string $title): bool
{
- $sku = Sku::withObjectTenantContext($this)->where('title', $title)->first();
+ $sku = $this->skuByTitle($title);
if (!$sku) {
return false;
@@ -234,6 +242,30 @@
}
}
+ /**
+ * Find the SKU object by title. Use current object's tenant context.
+ *
+ * @param string $title SKU title.
+ *
+ * @return ?\App\Sku A SKU object
+ */
+ protected function skuByTitle(string $title): ?Sku
+ {
+ return Sku::withObjectTenantContext($this)->where('title', $title)->first();
+ }
+
+ /**
+ * Returns entitleable object title (e.g. email or domain name).
+ *
+ * @return string|null An object title/name
+ */
+ public function toString(): ?string
+ {
+ // This method should be overloaded by the model class
+ // if the object has not email attribute
+ return $this->email;
+ }
+
/**
* Returns the wallet by which the object is controlled
*
diff --git a/src/app/Traits/Meet/RoomConfigTrait.php b/src/app/Traits/Meet/RoomConfigTrait.php
new file mode 100644
--- /dev/null
+++ b/src/app/Traits/Meet/RoomConfigTrait.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Traits\Meet;
+
+trait RoomConfigTrait
+{
+ /**
+ * A helper to get the room configuration.
+ */
+ public function getConfig(): array
+ {
+ $settings = $this->getSettings(['password', 'locked', 'nomedia']);
+
+ $config = [
+ 'acl' => $this->getACL(),
+ 'locked' => $settings['locked'] === 'true',
+ 'nomedia' => $settings['nomedia'] === 'true',
+ 'password' => $settings['password'],
+ ];
+
+ return $config;
+ }
+
+ /**
+ * A helper to update room configuration.
+ *
+ * @param array $config An array of configuration options
+ *
+ * @return array A list of input validation error messages
+ */
+ public function setConfig(array $config): array
+ {
+ $errors = [];
+
+ foreach ($config as $key => $value) {
+ if ($key == 'password') {
+ if ($value === null || $value === '') {
+ $value = null;
+ } else {
+ // TODO: Do we have to validate the password in any way?
+ }
+ $this->setSetting($key, $value);
+ } elseif ($key == 'locked' || $key == 'nomedia') {
+ $this->setSetting($key, $value ? 'true' : null);
+ } elseif ($key == 'acl') {
+ if (!empty($value) && !$this->hasSKU('group-room')) {
+ $errors[$key] = \trans('validation.invalid-config-parameter');
+ continue;
+ }
+
+ $acl_errors = $this->validateACL($value);
+
+ if (empty($acl_errors)) {
+ $this->setACL($value);
+ } else {
+ $errors[$key] = $acl_errors;
+ }
+ } else {
+ $errors[$key] = \trans('validation.invalid-config-parameter');
+ }
+ }
+
+ return $errors;
+ }
+}
diff --git a/src/app/Traits/PermissibleTrait.php b/src/app/Traits/PermissibleTrait.php
new file mode 100644
--- /dev/null
+++ b/src/app/Traits/PermissibleTrait.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace App\Traits;
+
+use App\Permission;
+use Illuminate\Support\Facades\Validator;
+
+trait PermissibleTrait
+{
+ /**
+ * Boot function from Laravel.
+ */
+ protected static function bootPermissibleTrait()
+ {
+ // Selete object's shares on object's delete
+ static::deleting(function ($model) {
+ $model->permissions()->delete();
+ });
+ }
+
+ /**
+ * Permissions for this object.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function permissions()
+ {
+ return $this->hasMany(Permission::class, 'permissible_id', 'id')
+ ->where('permissible_type', self::class);
+ }
+
+ /**
+ * Validate ACL input
+ *
+ * @param mixed $input Common ACL input
+ *
+ * @return array List of validation errors
+ */
+ protected function validateACL(&$input): array
+ {
+ if (!is_array($input)) {
+ $input = (array) $input;
+ }
+
+ $users = [];
+ $errors = [];
+ $supported = $this->supportedACL();
+
+ foreach ($input as $i => $v) {
+ if (!is_string($v) || empty($v) || !substr_count($v, ',')) {
+ $errors[$i] = \trans('validation.acl-entry-invalid');
+ } else {
+ list($user, $acl) = explode(',', $v, 2);
+ $user = trim($user);
+ $acl = trim($acl);
+ $error = null;
+
+ if (!isset($supported[$acl])) {
+ $errors[$i] = \trans('validation.acl-permission-invalid');
+ } elseif (in_array($user, $users) || ($error = $this->validateACLIdentifier($user))) {
+ $errors[$i] = $error ?: \trans('validation.acl-entry-invalid');
+ }
+
+ $input[$i] = "$user, $acl";
+ $users[] = $user;
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Validate an ACL identifier.
+ *
+ * @param string $identifier Email address
+ *
+ * @return ?string Error message on validation error
+ */
+ protected function validateACLIdentifier(string $identifier): ?string
+ {
+ $v = Validator::make(['email' => $identifier], ['email' => 'required|email']);
+
+ if ($v->fails()) {
+ return \trans('validation.emailinvalid');
+ }
+
+ $user = \App\User::where('email', \strtolower($identifier))->first();
+
+ if ($user) {
+ return null;
+ }
+
+ return \trans('validation.notalocaluser');
+ }
+
+ /**
+ * Build an ACL list from the object's permissions
+ *
+ * @return array ACL list in a "common" format
+ */
+ protected function getACL(): array
+ {
+ $supported = $this->supportedACL();
+
+ return $this->permissions()->get()
+ ->map(function ($permission) use ($supported) {
+ $acl = array_search($permission->rights, $supported) ?: 'none';
+ return "{$permission->user}, {$acl}";
+ })
+ ->all();
+ }
+
+ /**
+ * Update the permissions based on the ACL input.
+ *
+ * @param array $acl ACL list in a "common" format
+ */
+ protected function setACL(array $acl): void
+ {
+ $users = [];
+ $supported = $this->supportedACL();
+
+ foreach ($acl as $item) {
+ list($user, $right) = explode(',', $item, 2);
+ $users[\strtolower($user)] = $supported[trim($right)] ?? 0;
+ }
+
+ // Compare the input with existing shares
+ $this->permissions()->get()->each(function ($permission) use (&$users) {
+ if (isset($users[$permission->user])) {
+ if ($permission->rights != $users[$permission->user]) {
+ $permission->rights = $users[$permission->user];
+ $permission->save();
+ }
+ unset($users[$permission->user]);
+ } else {
+ $permission->delete();
+ }
+ });
+
+ foreach ($users as $user => $rights) {
+ $this->permissions()->create([
+ 'user' => $user,
+ 'rights' => $rights,
+ 'permissible_type' => self::class,
+ ]);
+ }
+ }
+
+ /**
+ * Returns a map of supported ACL labels.
+ *
+ * @return array Map of supported share rights/ACL labels
+ */
+ protected function supportedACL(): array
+ {
+ return [
+ 'read-only' => Permission::RIGHT_READ,
+ 'read-write' => Permission::RIGHT_READ | Permission::RIGHT_WRITE,
+ 'full' => Permission::RIGHT_ADMIN,
+ ];
+ }
+}
diff --git a/src/app/Transaction.php b/src/app/Transaction.php
--- a/src/app/Transaction.php
+++ b/src/app/Transaction.php
@@ -158,7 +158,7 @@
$discount = $entitlement->wallet->getDiscountRate();
$result['entitlement_cost'] = $cost * $discount;
- $result['object'] = $entitlement->entitleableTitle();
+ $result['object'] = $entitlement->entitleable->toString();
$result['sku_title'] = $entitlement->sku->title;
} else {
$wallet = $this->wallet();
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -434,6 +434,19 @@
return $this->entitleables(Resource::class, $with_accounts);
}
+ /**
+ * Return rooms controlled by the current user.
+ *
+ * @param bool $with_accounts Include rooms assigned to wallets
+ * the current user controls but not owns.
+ *
+ * @return \Illuminate\Database\Eloquent\Builder Query builder
+ */
+ public function rooms($with_accounts = true)
+ {
+ return $this->entitleables(Meet\Room::class, $with_accounts);
+ }
+
/**
* Return shared folders controlled by the current user.
*
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -354,6 +354,18 @@
return (100 - $this->getDiscount()) / 100;
}
+ /**
+ * Check if the specified user is a controller to this wallet.
+ *
+ * @param \App\User $user The user object.
+ *
+ * @return bool True if the user is one of the wallet controllers (including user), False otherwise
+ */
+ public function isController(User $user): bool
+ {
+ return $user->id == $this->user_id || $this->controllers->contains($user);
+ }
+
/**
* A helper to display human-readable amount of money using
* the wallet currency and specified locale.
diff --git a/src/database/migrations/2021_10_27_120000_extend_openvidu_rooms_session_id.php b/src/database/migrations/2021_10_27_120000_extend_openvidu_rooms_session_id.php
--- a/src/database/migrations/2021_10_27_120000_extend_openvidu_rooms_session_id.php
+++ b/src/database/migrations/2021_10_27_120000_extend_openvidu_rooms_session_id.php
@@ -32,7 +32,7 @@
Schema::table(
'openvidu_rooms',
function (Blueprint $table) {
- $table->string('session_id', 16)->change();
+ // $table->string('session_id', 16)->change();
}
);
}
diff --git a/src/database/migrations/2022_05_13_100000_permissions_and_room_subscriptions.php b/src/database/migrations/2022_05_13_100000_permissions_and_room_subscriptions.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2022_05_13_100000_permissions_and_room_subscriptions.php
@@ -0,0 +1,150 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ */
+ public function up(): void
+ {
+ Schema::create(
+ 'permissions',
+ function (Blueprint $table) {
+ $table->string('id', 36)->primary();
+ $table->bigInteger('permissible_id');
+ $table->string('permissible_type');
+ $table->integer('rights')->default(0);
+ $table->string('user');
+ $table->timestamps();
+
+ $table->index('user');
+ $table->index(['permissible_id', 'permissible_type']);
+ }
+ );
+
+ Schema::table(
+ 'openvidu_rooms',
+ function (Blueprint $table) {
+ $table->bigInteger('tenant_id')->unsigned()->nullable();
+ $table->string('description')->nullable();
+ $table->softDeletes();
+
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ }
+ );
+
+ // Create the new SKUs
+ if (!\App\Sku::where('title', 'room')->first()) {
+ $sku = \App\Sku::create([
+ 'title' => 'group-room',
+ 'name' => 'Group conference room',
+ 'description' => 'Shareable audio & video conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\GroupRoom',
+ 'active' => true,
+ ]);
+
+ $sku = \App\Sku::create([
+ 'title' => 'room',
+ 'name' => 'Standard conference room',
+ 'description' => 'Audio & video conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Room',
+ 'active' => true,
+ ]);
+
+ // Create the entitlement for every existing room
+ foreach (\App\Meet\Room::get() as $room) {
+ $user = \App\User::find($room->user_id); // @phpstan-ignore-line
+ if (!$user) {
+ $room->forceDelete();
+ continue;
+ }
+
+ // Set tenant_id
+ if ($user->tenant_id) {
+ $room->tenant_id = $user->tenant_id;
+ $room->save();
+ }
+
+ $wallet = $user->wallets()->first();
+
+ \App\Entitlement::create([
+ 'wallet_id' => $wallet->id,
+ 'sku_id' => $sku->id,
+ 'cost' => 0,
+ 'fee' => 0,
+ 'entitleable_id' => $room->id,
+ 'entitleable_type' => \App\Meet\Room::class
+ ]);
+ }
+ }
+
+ // Remove 'meet' SKU/entitlements
+ \App\Sku::where('title', 'meet')->delete();
+
+ Schema::table(
+ 'openvidu_rooms',
+ function (Blueprint $table) {
+ $table->dropForeign('openvidu_rooms_user_id_foreign');
+ $table->dropColumn('user_id');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table(
+ 'openvidu_rooms',
+ function (Blueprint $table) {
+ $table->dropForeign('openvidu_rooms_tenant_id_foreign');
+ $table->dropColumn('tenant_id');
+ $table->dropColumn('description');
+ $table->dropSoftDeletes();
+
+ $table->bigInteger('user_id')->nullable();
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ }
+ );
+
+ // Set user_id back
+ foreach (\App\Meet\Room::get() as $room) {
+ $wallet = $room->wallet();
+ if (!$wallet) {
+ $room->forceDelete();
+ continue;
+ }
+
+ $room->user_id = $wallet->user_id; // @phpstan-ignore-line
+ $room->save();
+ }
+
+ \App\Entitlement::where('entitleable_type', \App\Meet\Room::class)->forceDelete();
+ \App\Sku::where('title', 'room')->delete();
+ \App\Sku::where('title', 'group-room')->delete();
+
+ \App\Sku::create([
+ 'title' => 'meet',
+ 'name' => 'Voice & Video Conferencing (public beta)',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Meet',
+ 'active' => true,
+ ]);
+
+ Schema::dropIfExists('permissions');
+ }
+};
diff --git a/src/database/seeds/local/MeetRoomSeeder.php b/src/database/seeds/local/MeetRoomSeeder.php
--- a/src/database/seeds/local/MeetRoomSeeder.php
+++ b/src/database/seeds/local/MeetRoomSeeder.php
@@ -2,7 +2,6 @@
namespace Database\Seeds\Local;
-use App\Meet\Room;
use Illuminate\Database\Seeder;
class MeetRoomSeeder extends Seeder
@@ -15,20 +14,26 @@
public function run()
{
$john = \App\User::where('email', 'john@kolab.org')->first();
- $jack = \App\User::where('email', 'jack@kolab.org')->first();
+ $wallet = $john->wallets()->first();
- \App\Meet\Room::create(
+ $rooms = [
[
- 'user_id' => $john->id,
- 'name' => 'john'
- ]
- );
-
- \App\Meet\Room::create(
+ 'name' => 'john',
+ 'description' => "Standard room"
+ ],
[
- 'user_id' => $jack->id,
- 'name' => strtolower(\App\Utils::randStr(3, 3, '-'))
+ 'name' => 'shared',
+ 'description' => "Shared room"
]
- );
+ ];
+
+ foreach ($rooms as $idx => $room) {
+ $room = \App\Meet\Room::create($room);
+ $rooms[$idx] = $room;
+ }
+
+ $rooms[0]->assignToWallet($wallet, 'room');
+ $rooms[1]->assignToWallet($wallet, 'group-room');
+ $rooms[1]->setConfig(['acl' => 'jack@kolab.org, full']);
}
}
diff --git a/src/database/seeds/local/SkuSeeder.php b/src/database/seeds/local/SkuSeeder.php
--- a/src/database/seeds/local/SkuSeeder.php
+++ b/src/database/seeds/local/SkuSeeder.php
@@ -14,7 +14,7 @@
*/
public function run()
{
- Sku::create(
+ $skus = [
[
'title' => 'mailbox',
'name' => 'User Mailbox',
@@ -24,10 +24,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Mailbox',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain',
'name' => 'Hosted Domain',
@@ -36,10 +33,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Domain',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-registration',
'name' => 'Domain Registration',
@@ -48,10 +42,7 @@
'period' => 'yearly',
'handler_class' => 'App\Handlers\DomainRegistration',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-hosting',
'name' => 'External Domain',
@@ -61,10 +52,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainHosting',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-relay',
'name' => 'Domain Relay',
@@ -73,10 +61,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainRelay',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'storage',
'name' => 'Storage Quota',
@@ -86,10 +71,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Storage',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'groupware',
'name' => 'Groupware Features',
@@ -99,10 +81,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Groupware',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'resource',
'name' => 'Resource',
@@ -111,10 +90,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Resource',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'shared-folder',
'name' => 'Shared Folder',
@@ -123,10 +99,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\SharedFolder',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => '2fa',
'name' => '2-Factor Authentication',
@@ -136,10 +109,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Auth2F',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'activesync',
'name' => 'Activesync',
@@ -149,166 +119,132 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Activesync',
'active' => true,
- ]
- );
-
- // Check existence because migration might have added this already
- $sku = Sku::where(['title' => 'beta', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- Sku::create(
- [
- 'title' => 'beta',
- 'name' => 'Private Beta (invitation only)',
- 'description' => 'Access to the private beta program features',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Beta',
- 'active' => false,
- ]
- );
- }
-
- // Check existence because migration might have added this already
- $sku = Sku::where(['title' => 'meet', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- Sku::create(
- [
- 'title' => 'meet',
- 'name' => 'Voice & Video Conferencing (public beta)',
- 'description' => 'Video conferencing tool',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Meet',
- 'active' => true,
- ]
- );
+ ],
+ [
+ 'title' => 'beta',
+ 'name' => 'Private Beta (invitation only)',
+ 'description' => 'Access to the private beta program features',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ],
+ [
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'group-room',
+ 'name' => 'Group conference room',
+ 'description' => 'Shareable audio & video conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\GroupRoom',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'room',
+ 'name' => 'Standard conference room',
+ 'description' => 'Audio & video conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Room',
+ 'active' => true,
+ ],
+ ];
+
+ foreach ($skus as $sku) {
+ // Check existence because migration might have added this already
+ if (!Sku::where('title', $sku['title'])->where('tenant_id', \config('app.tenant_id'))->first()) {
+ Sku::create($sku);
+ }
}
- // Check existence because migration might have added this already
- $sku = Sku::where(['title' => 'group', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- Sku::create(
- [
- 'title' => 'group',
- 'name' => 'Group',
- 'description' => 'Distribution list',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Group',
- 'active' => true,
- ]
- );
- }
+ $skus = [
+ [
+ 'title' => 'mailbox',
+ 'name' => 'User Mailbox',
+ 'description' => 'Just a mailbox',
+ 'cost' => 500,
+ 'fee' => 333,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Mailbox',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'storage',
+ 'name' => 'Storage Quota',
+ 'description' => 'Some wiggle room',
+ 'cost' => 25,
+ 'fee' => 16,
+ 'units_free' => 5,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Storage',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'domain-hosting',
+ 'name' => 'External Domain',
+ 'description' => 'Host a domain that is externally registered',
+ 'cost' => 100,
+ 'fee' => 66,
+ 'units_free' => 1,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\DomainHosting',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'groupware',
+ 'name' => 'Groupware Features',
+ 'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.',
+ 'cost' => 490,
+ 'fee' => 327,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Groupware',
+ 'active' => true,
+ ],
+ [
+ 'title' => '2fa',
+ 'name' => '2-Factor Authentication',
+ 'description' => 'Two factor authentication for webmail and administration panel',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Auth2F',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'activesync',
+ 'name' => 'Activesync',
+ 'description' => 'Mobile synchronization',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Activesync',
+ 'active' => true,
+ ],
+ ];
// for tenants that are not the configured tenant id
$tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get();
foreach ($tenants as $tenant) {
- $sku = Sku::create(
- [
- 'title' => 'mailbox',
- 'name' => 'User Mailbox',
- 'description' => 'Just a mailbox',
- 'cost' => 500,
- 'fee' => 333,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Mailbox',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => 'storage',
- 'name' => 'Storage Quota',
- 'description' => 'Some wiggle room',
- 'cost' => 25,
- 'fee' => 16,
- 'units_free' => 5,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Storage',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => 'domain-hosting',
- 'name' => 'External Domain',
- 'description' => 'Host a domain that is externally registered',
- 'cost' => 100,
- 'fee' => 66,
- 'units_free' => 1,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\DomainHosting',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => 'groupware',
- 'name' => 'Groupware Features',
- 'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.',
- 'cost' => 490,
- 'fee' => 327,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Groupware',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => '2fa',
- 'name' => '2-Factor Authentication',
- 'description' => 'Two factor authentication for webmail and administration panel',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Auth2F',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => 'activesync',
- 'name' => 'Activesync',
- 'description' => 'Mobile synchronization',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Activesync',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
+ foreach ($skus as $sku) {
+ $sku = Sku::create($sku);
+ $sku->tenant_id = $tenant->id;
+ $sku->save();
+ }
}
}
}
diff --git a/src/database/seeds/production/SkuSeeder.php b/src/database/seeds/production/SkuSeeder.php
--- a/src/database/seeds/production/SkuSeeder.php
+++ b/src/database/seeds/production/SkuSeeder.php
@@ -14,7 +14,7 @@
*/
public function run()
{
- Sku::create(
+ $skus = [
[
'title' => 'mailbox',
'name' => 'User Mailbox',
@@ -24,10 +24,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Mailbox',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain',
'name' => 'Hosted Domain',
@@ -36,10 +33,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Domain',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-registration',
'name' => 'Domain Registration',
@@ -48,10 +42,7 @@
'period' => 'yearly',
'handler_class' => 'App\Handlers\DomainRegistration',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-hosting',
'name' => 'External Domain',
@@ -61,10 +52,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainHosting',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-relay',
'name' => 'Domain Relay',
@@ -73,10 +61,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainRelay',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'storage',
'name' => 'Storage Quota',
@@ -86,10 +71,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Storage',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'groupware',
'name' => 'Groupware Features',
@@ -99,10 +81,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Groupware',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'resource',
'name' => 'Resource',
@@ -111,10 +90,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Resource',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'shared-folder',
'name' => 'Shared Folder',
@@ -123,10 +99,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\SharedFolder',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => '2fa',
'name' => '2-Factor Authentication',
@@ -136,10 +109,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Auth2F',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'activesync',
'name' => 'Activesync',
@@ -149,55 +119,54 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Activesync',
'active' => true,
- ]
- );
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'beta')->first()) {
- Sku::create(
- [
- 'title' => 'beta',
- 'name' => 'Private Beta (invitation only)',
- 'description' => 'Access to the private beta program subscriptions',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Beta',
- 'active' => false,
- ]
- );
- }
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'meet')->first()) {
- Sku::create(
- [
- 'title' => 'meet',
- 'name' => 'Voice & Video Conferencing (public beta)',
- 'description' => 'Video conferencing tool',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Meet',
- 'active' => true,
- ]
- );
- }
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'group')->first()) {
- Sku::create(
- [
- 'title' => 'group',
- 'name' => 'Group',
- 'description' => 'Distribution list',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Group',
- 'active' => true,
- ]
- );
+ ],
+ [
+ 'title' => 'beta',
+ 'name' => 'Private Beta (invitation only)',
+ 'description' => 'Access to the private beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ],
+ [
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'group-room',
+ 'name' => 'Group conference room',
+ 'description' => 'Shareable audio & video conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\GroupRoom',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'room',
+ 'name' => 'Standard conference room',
+ 'description' => 'Audio & video conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Room',
+ 'active' => true,
+ ],
+ ];
+
+ foreach ($skus as $sku) {
+ // Check existence because migration might have added this already
+ if (!Sku::where('title', $sku['title'])->first()) {
+ Sku::create($sku);
+ }
}
}
}
diff --git a/src/resources/js/meet/room.js b/src/resources/js/meet/room.js
--- a/src/resources/js/meet/room.js
+++ b/src/resources/js/meet/room.js
@@ -739,7 +739,6 @@
// TODO: This probably could be better done with css
let elements = {
- '.dropdown-menu': withMenu,
'.permissions': withPerm,
'.interpreting': withPerm && rolePublisher,
'svg.moderator': roleModerator,
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
@@ -16,9 +16,10 @@
const DomainListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Domain/List')
const FileInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/Info')
const FileListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/List')
-const MeetComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Rooms')
const ResourceInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/Info')
const ResourceListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/List')
+const RoomInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Room/Info')
+const RoomListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Room/List')
const SettingsComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Settings')
const SharedFolderInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/SharedFolder/Info')
const SharedFolderListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/SharedFolder/List')
@@ -27,7 +28,7 @@
const UserProfileComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/User/Profile')
const UserProfileDeleteComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/User/ProfileDelete')
const WalletComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Wallet')
-const RoomComponent = () => import(/* webpackChunkName: "../user/meet" */ '../../vue/Meet/Room.vue')
+const MeetComponent = () => import(/* webpackChunkName: "../user/meet" */ '../../vue/Meet/Room.vue')
const routes = [
{
@@ -88,6 +89,12 @@
name: 'logout',
component: LogoutComponent
},
+ {
+ name: 'meet',
+ path: '/meet/:room',
+ component: MeetComponent,
+ meta: { loading: true }
+ },
{
path: '/password-reset/:code?',
name: 'password-reset',
@@ -118,16 +125,16 @@
meta: { requiresAuth: true, perm: 'resources' }
},
{
- component: RoomComponent,
+ path: '/room/:room',
name: 'room',
- path: '/meet/:room',
- meta: { loading: true }
+ component: RoomInfoComponent,
+ meta: { requiresAuth: true, perm: 'rooms' }
},
{
path: '/rooms',
name: 'rooms',
- component: MeetComponent,
- meta: { requiresAuth: true }
+ component: RoomListComponent,
+ meta: { requiresAuth: true, perm: 'rooms' }
},
{
path: '/settings',
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
@@ -82,6 +82,12 @@
'resource-delete-success' => 'Resource deleted successfully.',
'resource-setconfig-success' => 'Resource settings updated successfully.',
+ 'room-update-success' => 'Room updated successfully.',
+ 'room-create-success' => 'Room created successfully.',
+ 'room-delete-success' => 'Room deleted successfully.',
+ 'room-setconfig-success' => 'Room configuration updated successfully.',
+ 'room-unsupported-option-error' => 'Invalid room configuration option.',
+
'shared-folder-update-success' => 'Shared folder updated successfully.',
'shared-folder-create-success' => 'Shared folder created successfully.',
'shared-folder-delete-success' => 'Shared folder deleted successfully.',
diff --git a/src/resources/lang/en/meet.php b/src/resources/lang/en/meet.php
--- a/src/resources/lang/en/meet.php
+++ b/src/resources/lang/en/meet.php
@@ -16,8 +16,6 @@
'connection-not-found' => 'The connection does not exist.',
'connection-dismiss-error' => 'Failed to dismiss the connection.',
'room-not-found' => 'The room does not exist.',
- 'room-setconfig-success' => 'Room configuration updated successfully.',
- 'room-unsupported-option-error' => 'Invalid room configuration option.',
'session-not-found' => 'The session does not exist.',
'session-create-error' => 'Failed to create the session.',
'session-join-error' => 'Failed to join the session.',
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
@@ -215,31 +215,6 @@
],
'meet' => [
- 'title' => "Voice & Video Conferencing",
- 'welcome' => "Welcome to our beta program for Voice & Video Conferencing.",
- 'url' => "You have a room of your own at the URL below. This room is only open when you yourself are in attendance. Use this URL to invite people to join you.",
- 'notice' => "This is a work in progress and more features will be added over time. Current features include:",
- 'sharing' => "Screen Sharing",
- 'sharing-text' => "Share your screen for presentations or show-and-tell.",
- 'security' => "Room Security",
- 'security-text' => "Increase the room security by setting a password that attendees will need to know"
- . " before they can enter, or lock the door so attendees will have to knock, and a moderator can accept or deny those requests.",
- 'qa-title' => "Raise Hand (Q&A)",
- 'qa-text' => "Silent audience members can raise their hand to facilitate a Question & Answer session with the panel members.",
- 'moderation' => "Moderator Delegation",
- 'moderation-text' => "Delegate moderator authority for the session, so that a speaker is not needlessly"
- . " interrupted with attendees knocking and other moderator duties.",
- 'eject' => "Eject Attendees",
- 'eject-text' => "Eject attendees from the session in order to force them to reconnect, or address policy"
- . " violations. Click the user icon for effective dismissal.",
- 'silent' => "Silent Audience Members",
- 'silent-text' => "For a webinar-style session, configure the room to force all new attendees to be silent audience members.",
- 'interpreters' => "Language Specific Audio Channels",
- 'interpreters-text' => "Designate a participant to interpret the original audio to a target language, for sessions"
- . " with multi-lingual attendees. The interpreter is expected to be able to relay the original audio, and override it.",
- 'beta-notice' => "Keep in mind that this is still in beta and might come with some issues."
- . " Should you encounter any on your way, let us know by contacting support.",
-
// Room options dialog
'options' => "Room options",
'password' => "Password",
@@ -355,6 +330,22 @@
'new' => "New resource",
],
+ 'room' => [
+ 'create' => "Create room",
+ 'delete' => "Delete room",
+ 'description-hint' => "This is an optional short description for the room, so you can find it more easily on the list.",
+ 'goto' => "Enter the room",
+ 'list-empty' => "There are no conference rooms in this account.",
+ 'list-empty-nocontroller' => "Do you need a room? Ask your account owner to create one and share it with you.",
+ 'list-title' => "Voice & video conferencing rooms",
+ 'moderators' => "Moderators",
+ 'moderators-text' => "You can share your room with other users. They will become the room moderators with all moderator powers and ability to open the room without your presence.",
+ 'new' => "New room",
+ 'new-hint' => "We'll generate a unique name for the room that will then allow you to access the room.",
+ 'title' => "Room: {name}",
+ 'url' => "You can access the room at the URL below. Use this URL to invite people to join you. This room is only open when you (or another room moderator) is in attendance.",
+ ],
+
'settings' => [
'password-policy' => "Password Policy",
'password-retention' => "Password Retention",
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -153,6 +153,7 @@
'spf-entry-invalid' => 'The entry format is invalid. Expected a domain name starting with a dot.',
'sp-entry-invalid' => 'The entry format is invalid. Expected an email, domain, or part of it.',
'acl-entry-invalid' => 'The entry format is invalid. Expected an email address.',
+ 'acl-permission-invalid' => 'The specified permission is invalid.',
'file-perm-exists' => 'File permission already exists.',
'file-perm-invalid' => 'The file permission is invalid.',
'file-name-exists' => 'The file name already exists.',
diff --git a/src/resources/lang/fr/app.php b/src/resources/lang/fr/app.php
--- a/src/resources/lang/fr/app.php
+++ b/src/resources/lang/fr/app.php
@@ -74,6 +74,9 @@
'resource-delete-success' => "Ressource suprimmée avec succès.",
'resource-setconfig-success' => "Les paramètres des ressources ont été mis à jour avec succès.",
+ 'room-setconfig-success' => 'La configuration de la salle a été actualisée avec succès.',
+ 'room-unsupported-option-error' => 'Option de configuration de la salle invalide.',
+
'shared-folder-update-success' => "Dossier partagé mis à jour avec succès.",
'shared-folder-create-success' => "Dossier partagé créé avec succès.",
'shared-folder-delete-success' => "Dossier partagé supprimé avec succès.",
diff --git a/src/resources/lang/fr/meet.php b/src/resources/lang/fr/meet.php
--- a/src/resources/lang/fr/meet.php
+++ b/src/resources/lang/fr/meet.php
@@ -16,8 +16,6 @@
'connection-not-found' => 'La connexion n´existe pas.',
'connection-dismiss-error' => 'Échec du rejet de la connexion.',
'room-not-found' => 'La salle n´existe pas.',
- 'room-setconfig-success' => 'La configuration de la salle a été actualisée avec succès.',
- 'room-unsupported-option-error' => 'Option de configuration de la salle invalide.',
'session-not-found' => 'La session n\'existe pas.',
'session-create-error' => 'Échec de la création de la session.',
'session-join-error' => 'Échec de se joindre à la session.',
diff --git a/src/resources/lang/fr/ui.php b/src/resources/lang/fr/ui.php
--- a/src/resources/lang/fr/ui.php
+++ b/src/resources/lang/fr/ui.php
@@ -131,6 +131,7 @@
'phone' => "Téléphone",
'shared-folder' => "Dossier partagé",
'status' => "État",
+ 'subscriptions' => "Subscriptions",
'surname' => "Nom de famille",
'type' => "Type",
'user' => "Utilisateur",
@@ -176,9 +177,7 @@
],
'meet' => [
- 'title' => "Voix et vidéo-conférence",
'welcome' => "Bienvenue dans notre programme bêta pour les conférences vocales et vidéo.",
- 'url' => "Vous disposez d'une salle avec l'URL ci-dessous. Cette salle ouvre uniquement quand vous y êtes vous-même. Utilisez cette URL pour inviter des personnes à vous rejoindre.",
'notice' => "Il s'agit d'un travail en évolution et d'autres fonctions seront ajoutées au fil du temps. Les fonctions actuelles sont les suivantes:",
'sharing' => "Partage d'écran",
'sharing-text' => "Partagez votre écran pour des présentations ou des exposés.",
@@ -438,7 +437,6 @@
'search-pl' => "ID utilisateur, e-mail ou domamine",
'skureq' => "{sku} demande {list}.",
'subscription' => "Subscription",
- 'subscriptions' => "Subscriptions",
'subscriptions-none' => "Cet utilisateur n'a pas de subscriptions.",
'users' => "Utilisateurs",
],
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
@@ -28,11 +28,11 @@
<svg-icon icon="wallet"></svg-icon><span>{{ $t('dashboard.wallet') }}</span>
<span v-if="balance < 0" class="badge bg-danger">{{ $root.price(balance, currency) }}</span>
</router-link>
- <router-link v-if="$root.hasSKU('meet') && !$root.isDegraded()" class="card link-chat" :to="{ name: 'rooms' }">
+ <router-link v-if="status.enableRooms" class="card link-chat" :to="{ name: 'rooms' }">
<svg-icon icon="comments"></svg-icon><span>{{ $t('dashboard.chat') }}</span>
<span class="badge bg-primary">{{ $t('dashboard.beta') }}</span>
</router-link>
- <router-link v-if="status.enableFiles && !$root.isDegraded()" class="card link-files" :to="{ name: 'files' }">
+ <router-link v-if="status.enableFiles" class="card link-files" :to="{ name: 'files' }">
<svg-icon icon="folder-closed"></svg-icon><span>{{ $t('dashboard.files') }}</span>
<span class="badge bg-primary">{{ $t('dashboard.beta') }}</span>
</router-link>
diff --git a/src/resources/vue/Meet/RoomOptions.vue b/src/resources/vue/Meet/RoomOptions.vue
--- a/src/resources/vue/Meet/RoomOptions.vue
+++ b/src/resources/vue/Meet/RoomOptions.vue
@@ -57,7 +57,7 @@
configSave(name, value, callback) {
const post = { [name]: value }
- axios.post('/api/v4/meet/rooms/' + this.room + '/config', post)
+ axios.post('/api/v4/rooms/' + this.room + '/config', post)
.then(response => {
this.$set(this.config, name, value)
if (callback) {
diff --git a/src/resources/vue/Room/Info.vue b/src/resources/vue/Room/Info.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Room/Info.vue
@@ -0,0 +1,166 @@
+<template>
+ <div class="container">
+ <div id="room-info" class="card">
+ <div class="card-body">
+ <div class="card-title" v-if="room.id">
+ {{ $t('room.title', { name: room.name }) }}
+ <btn v-if="room.canDelete" class="btn-outline-danger button-delete float-end" @click="roomDelete" icon="trash-can">{{ $t('room.delete') }}</btn>
+ </div>
+ <div class="card-title" v-else>{{ $t('room.new') }}</div>
+ <div class="card-text">
+ <div id="room-intro" class="pt-2">
+ <p v-if="room.id">{{ $t('room.url') }}</p>
+ <p v-if="room.id" class="text-center"><router-link :to="roomRoute">{{ href }}</router-link></p>
+ <p v-if="!room.id">{{ $t('room.new-hint') }}</p>
+ </div>
+ <tabs class="mt-3" :tabs="tabs"></tabs>
+ <div class="tab-content">
+ <div v-if="!room.id || room.isOwner" class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
+ <form @submit.prevent="submit" class="card-body">
+ <div class="row mb-3">
+ <label for="description" class="col-sm-4 col-form-label">{{ $t('form.description') }}</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="description" v-model="room.description">
+ <small class="form-text">{{ $t('room.description-hint') }}</small>
+ </div>
+ </div>
+ <div v-if="room_id === 'new' || room.isOwner" id="room-skus" class="row mb-3">
+ <label class="col-sm-4 col-form-label">{{ $t('form.subscriptions') }}</label>
+ <subscription-select class="col-sm-8 pt-sm-1" ref="skus" :object="room" type="room"></subscription-select>
+ </div>
+ <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
+ </form>
+ </div>
+ <div v-if="room.canUpdate" :class="'tab-pane' + (!tabs.includes('form.general') ? ' show active' : '')" id="settings" role="tabpanel" aria-labelledby="tab-settings">
+ <form @submit.prevent="submitSettings" class="card-body">
+ <div class="row mb-3">
+ <label for="password" class="col-sm-4 col-form-label">{{ $t('form.password') }}</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" v-model="room.config.password">
+ <span class="form-text">{{ $t('meet.password-text') }}</span>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="room-lock-input" class="col-sm-4 col-form-label">{{ $t('meet.lock') }}</label>
+ <div class="col-sm-8">
+ <input type="checkbox" id="room-lock-input" class="form-check-input d-block" v-model="room.config.locked">
+ <small class="form-text">{{ $t('meet.lock-text') }}</small>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="room-nomedia-input" class="col-sm-4 col-form-label">{{ $t('meet.nomedia') }}</label>
+ <div class="col-sm-8">
+ <input type="checkbox" id="room-nomedia-input" class="form-check-input d-block" v-model="room.config.nomedia">
+ <small class="form-text">{{ $t('meet.nomedia-text') }}</small>
+ </div>
+ </div>
+ <div v-if="room.canShare" class="row mb-3">
+ <label for="acl-input" class="col-sm-4 col-form-label">{{ $t('room.moderators') }}</label>
+ <div class="col-sm-8">
+ <acl-input id="acl" v-model="room.config.acl" :list="room.config.acl" :useronly="true" :types="['full']"></acl-input>
+ <small class="form-text">{{ $t('room.moderators-text') }}</small>
+ </div>
+ </div>
+ <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ import AclInput from '../Widgets/AclInput'
+ import SubscriptionSelect from '../Widgets/SubscriptionSelect'
+
+ export default {
+ components: {
+ AclInput,
+ SubscriptionSelect
+ },
+ data() {
+ return {
+ href: '',
+ room_id: '',
+ room: { config: { acl: [] } },
+ roomRoute: ''
+ }
+ },
+ computed: {
+ tabs() {
+ let tabs = []
+
+ if (!this.room.id || this.room.isOwner) {
+ tabs.push('form.general')
+ }
+
+ if (this.room.canUpdate) {
+ tabs.push('form.settings')
+ }
+
+ return tabs
+ },
+ },
+ created() {
+ this.room_id = this.$route.params.room
+
+ if (this.room_id != 'new') {
+ axios.get('/api/v4/rooms/' + this.room_id, { loader: true })
+ .then(response => {
+ this.room = response.data
+ this.roomRoute = '/meet/' + encodeURI(this.room.name)
+ this.href = window.config['app.url'] + this.roomRoute
+ })
+ .catch(this.$root.errorHandler)
+ }
+ },
+ mounted() {
+ $('#description').focus()
+ },
+ methods: {
+ roomDelete() {
+ axios.delete('/api/v4/rooms/' + this.room.id)
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$toast.success(response.data.message)
+ this.$router.push('/rooms')
+ }
+ })
+ },
+ submit() {
+ this.$root.clearFormValidation($('#general form'))
+
+ let method = 'post'
+ let location = '/api/v4/rooms'
+ let post = this.$root.pick(this.room, ['description'])
+
+ if (this.room.id) {
+ method = 'put'
+ location += '/' + this.room.id
+ }
+
+ if (this.$refs.skus) {
+ post.skus = this.$refs.skus.getSkus()
+ }
+
+ axios[method](location, post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ this.$router.push('/rooms')
+ })
+ },
+ submitSettings() {
+ this.$root.clearFormValidation($('#settings form'))
+
+ const post = this.$root.pick(this.room.config, [ 'password', 'acl', 'locked', 'nomedia' ])
+
+ axios.post('/api/v4/rooms/' + this.room.id + '/config', post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ })
+ }
+ }
+ }
+</script>
diff --git a/src/resources/vue/Room/List.vue b/src/resources/vue/Room/List.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Room/List.vue
@@ -0,0 +1,81 @@
+<template>
+ <div class="container">
+ <div id="rooms-list" class="card">
+ <div class="card-body">
+ <div class="card-title">
+ {{ $t('room.list-title') }}
+ <small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
+ <btn-router v-if="!$root.isDegraded() && $root.hasPermission('settings')" to="room/new" class="btn-success float-end" icon="comments">
+ {{ $t('room.create') }}
+ </btn-router>
+ </div>
+ <div class="card-text">
+ <list-table :list="rooms" :setup="setup">
+ <template #buttons="{ item }">
+ <btn class="btn-link p-0 ms-1 lh-1" @click="goto(item)" icon="arrow-up-right-from-square" :title="$t('room.goto')"></btn>
+ </template>
+ </list-table>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ import { ListTable } from '../Widgets/ListTools'
+ import { library } from '@fortawesome/fontawesome-svg-core'
+
+ library.add(
+ require('@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare').definition,
+ require('@fortawesome/free-solid-svg-icons/faComments').definition
+ )
+
+ export default {
+ components: {
+ ListTable
+ },
+ data() {
+ return {
+ rooms: []
+ }
+ },
+ computed: {
+ setup() {
+ let setup = {
+ buttons: true,
+ model: 'room',
+ columns: [
+ {
+ prop: 'name',
+ icon: 'comments',
+ link: true
+ },
+ {
+ prop: 'description',
+ link: true
+ }
+ ]
+ }
+
+ if (!this.$root.hasPermission('settings')) {
+ setup.footLabel = 'room.list-empty-nocontroller'
+ }
+
+ return setup
+ }
+ },
+ mounted() {
+ axios.get('/api/v4/rooms', { loader: true })
+ .then(response => {
+ this.rooms = response.data.list
+ })
+ .catch(this.$root.errorHandler)
+ },
+ methods: {
+ goto(room) {
+ const location = window.config['app.url'] + '/meet/' + encodeURI(room.name)
+ window.open(location, '_blank')
+ }
+ }
+ }
+</script>
diff --git a/src/resources/vue/Rooms.vue b/src/resources/vue/Rooms.vue
deleted file mode 100644
--- a/src/resources/vue/Rooms.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
- <div class="container" dusk="rooms-component">
- <div id="meet-rooms" class="card">
- <div class="card-body">
- <div class="card-title">{{ $t('meet.title') }} <small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small></div>
- <div class="card-text">
- <p>{{ $t('meet.welcome') }}</p>
- <p>{{ $t('meet.url') }}</p>
- <p><router-link v-if="href" :to="roomRoute">{{ href }}</router-link></p>
- <p>{{ $t('meet.notice') }}</p>
- <dl>
- <dt>{{ $t('meet.sharing') }}</dt>
- <dd>{{ $t('meet.sharing-text') }}</dd>
- <dt>{{ $t('meet.security') }}</dt>
- <dd>{{ $t('meet.security-text') }}</dd>
- <dt>{{ $t('meet.qa-title') }}</dt>
- <dd>{{ $t('meet.qa-text') }}</dd>
- <dt>{{ $t('meet.moderation') }}</dt>
- <dd>{{ $t('meet.moderation-text') }}</dd>
- <dt>{{ $t('meet.eject') }}</dt>
- <dd>{{ $t('meet.eject-text') }}</dd>
- <dt>{{ $t('meet.silent') }}</dt>
- <dd>{{ $t('meet.silent-text') }}</dd>
- <dt>{{ $t('meet.interpreters') }}</dt>
- <dd>{{ $t('meet.interpreters-text') }}</dd>
- </dl>
- <p>{{ $t('meet.beta-notice') }}</p>
- </div>
- </div>
- </div>
- </div>
-</template>
-
-<script>
- export default {
- data() {
- return {
- rooms: [],
- href: '',
- roomRoute: ''
- }
- },
- mounted() {
- if (!this.$root.hasSKU('meet') || this.$root.isDegraded()) {
- this.$root.errorPage(403)
- return
- }
-
- axios.get('/api/v4/meet/rooms', { loader: true })
- .then(response => {
- this.rooms = response.data.list
- if (response.data.count) {
- this.roomRoute = '/meet/' + encodeURI(this.rooms[0].name)
- this.href = window.config['app.url'] + this.roomRoute
- }
- })
- .catch(this.$root.errorHandler)
- }
- }
-</script>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -77,7 +77,7 @@
</div>
<div v-if="user_id !== 'new'" id="user-skus" class="row mb-3">
<label class="col-sm-4 col-form-label">{{ $t('form.subscriptions') }}</label>
- <subscription-select v-if="user.id" class="col-sm-8 pt-sm-1" :object="user"></subscription-select>
+ <subscription-select v-if="user.id" class="col-sm-8 pt-sm-1" :object="user" ref="skus"></subscription-select>
</div>
<btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
</form>
@@ -231,15 +231,7 @@
if (this.user_id !== 'new') {
method = 'put'
location += '/' + this.user_id
-
- let skus = {}
- $('#user-skus input[type=checkbox]:checked').each((idx, input) => {
- let id = $(input).val()
- let range = $(input).parents('tr').first().find('input[type=range]').val()
-
- skus[id] = range || 1
- })
- post.skus = skus
+ post.skus = this.$refs.skus.getSkus()
} else {
post.package = $('#user-packages input:checked').val()
}
diff --git a/src/resources/vue/Widgets/AclInput.vue b/src/resources/vue/Widgets/AclInput.vue
--- a/src/resources/vue/Widgets/AclInput.vue
+++ b/src/resources/vue/Widgets/AclInput.vue
@@ -6,7 +6,7 @@
<option value="anyone">{{ $t('form.anyone') }}</option>
</select>
<input :id="id + '-input'" type="text" class="form-control main-input" :placeholder="$t('form.email')" @keydown="keyDown">
- <select class="form-select acl" v-model="perm">
+ <select v-if="types.length > 1" class="form-select acl" v-model="perm">
<option v-for="t in types" :key="t" :value="t">{{ $t('form.acl-' + t) }}</option>
</select>
<a href="#" class="btn btn-outline-secondary" @click.prevent="addItem">
@@ -15,7 +15,7 @@
</div>
<div class="input-group" v-for="(item, index) in list" :key="index">
<input type="text" class="form-control" :value="aclIdent(item)" :readonly="aclIdent(item) == 'anyone'" :placeholder="$t('form.email')">
- <select class="form-select acl">
+ <select v-if="types.length > 1" class="form-select acl">
<option v-for="t in types" :key="t" :value="t" :selected="aclPerm(item) == t">{{ $t('form.acl-' + t) }}</option>
</select>
<a href="#" class="btn btn-outline-secondary" @click.prevent="deleteItem(index)">
@@ -26,17 +26,19 @@
</template>
<script>
+ const DEFAULT_TYPES = [ 'read-only', 'read-write', 'full' ]
+
export default {
props: {
list: { type: Array, default: () => [] },
id: { type: String, default: '' },
+ types: { type: Array, default: () => DEFAULT_TYPES },
useronly: { type: Boolean, default: false }
},
data() {
return {
mod: 'user',
perm: 'read-only',
- types: [ 'read-only', 'read-write', 'full' ]
}
},
mounted() {
@@ -68,7 +70,9 @@
value = 'anyone'
}
- this.$set(this.list, this.list.length, value + ', ' + this.perm)
+ const perm = this.types.length > 1 ? this.perm : this.types[0]
+
+ this.$set(this.list, this.list.length, value + ', ' + perm)
this.input.classList.remove('is-invalid')
this.input.value = ''
@@ -109,7 +113,7 @@
updateList() {
// Update this.list to the current state of the html elements
$(this.$el).children('.input-group:not(:first-child)').each((index, elem) => {
- const perm = $(elem).find('select.acl').val()
+ const perm = this.types.length > 1 ? $(elem).find('select.acl').val() : this.types[0]
const value = $(elem).find('input').val()
this.$set(this.list, index, value + ', ' + perm)
})
diff --git a/src/resources/vue/Widgets/SubscriptionSelect.vue b/src/resources/vue/Widgets/SubscriptionSelect.vue
--- a/src/resources/vue/Widgets/SubscriptionSelect.vue
+++ b/src/resources/vue/Widgets/SubscriptionSelect.vue
@@ -99,7 +99,7 @@
sku.cost = sku.nextCost
}
- if (!sku.readonly) {
+ if (!sku.readonly && this.object.skus) {
sku.enabled = false
}
}
@@ -107,9 +107,16 @@
return sku
})
- // Update all range inputs (and price)
this.$nextTick(() => {
+ // Update all range inputs (and price)
$(this.$el).find('input[type=range]').each((idx, elem) => { this.rangeUpdate(elem) })
+
+ // Mark 'exclusive' SKUs as readonly, they can't be unchecked
+ this.skus.forEach(item => {
+ if (item.exclusive && item.enabled) {
+ $('#s' + item.id).find('input[type=checkbox]')[0].readOnly = true
+ }
+ })
})
})
.catch(this.$root.errorHandler)
@@ -141,9 +148,9 @@
if (input.checked) {
// Check if a required SKU is selected, alert the user if not
- (sku.required || []).forEach(requiredHandler => {
+ (sku.required || []).forEach(handler => {
this.skus.forEach(item => {
- if (item.handler == requiredHandler) {
+ if (item.handler == handler) {
if (!$('#s' + item.id).find('input[type=checkbox]:checked').length) {
required.push(item.name)
}
@@ -155,6 +162,20 @@
input.checked = false
return alert(this.$t('user.skureq', { sku: sku.name, list: required.join(', ') }))
}
+
+ // Make sure there must be only one of 'exclusive' SKUs
+ if (sku.exclusive) {
+ input.readOnly = true
+
+ this.skus.forEach(item => {
+ if (sku.exclusive.includes(item.handler)) {
+ $('#s' + item.id).find('input[type=checkbox]').prop({
+ checked: false,
+ readonly: false
+ })
+ }
+ })
+ }
} else {
// Uncheck all dependent SKUs, e.g. when unchecking Groupware we also uncheck Activesync
// TODO: Should we display an alert instead?
@@ -166,10 +187,10 @@
}
// Uncheck+lock/unlock conflicting SKUs
- (sku.forbidden || []).forEach(forbiddenHandler => {
+ (sku.forbidden || []).forEach(handler => {
this.skus.forEach(item => {
let checkbox
- if (item.handler == forbiddenHandler && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
+ if (item.handler == handler && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
if (input.checked) {
checkbox.checked = false
checkbox.readOnly = true
@@ -208,6 +229,18 @@
// Update the price
record.find('.price').text(this.$root.priceLabel(cost, this.discount, this.currency))
+ },
+ getSkus() {
+ let skus = {}
+
+ $(this.$el).find('input[type=checkbox]:checked').each((idx, input) => {
+ let id = $(input).val()
+ let range = $(input).parents('tr').first().find('input[type=range]').val()
+
+ skus[id] = range || 1
+ })
+
+ return skus
}
}
}
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -101,8 +101,10 @@
Route::apiResource('packages', API\V4\PackagesController::class);
- Route::get('meet/rooms', [API\V4\MeetController::class, 'index']);
- Route::post('meet/rooms/{id}/config', [API\V4\MeetController::class, 'setRoomConfig']);
+ Route::apiResource('rooms', API\V4\RoomsController::class);
+ Route::post('rooms/{id}/config', [API\V4\RoomsController::class, 'setConfig']);
+ Route::get('rooms/{id}/skus', [API\V4\RoomsController::class, 'skus']);
+
Route::post('meet/rooms/{id}', [API\V4\MeetController::class, 'joinRoom'])
->withoutMiddleware(['auth:api']);
diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
--- a/src/tests/Browser/Admin/UserTest.php
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -40,7 +40,6 @@
Entitlement::where('cost', '>=', 5000)->delete();
$this->deleteTestGroup('group-test@kolab.org');
- $this->clearMeetEntitlements();
}
/**
@@ -62,7 +61,6 @@
Entitlement::where('cost', '>=', 5000)->delete();
$this->deleteTestGroup('group-test@kolab.org');
- $this->clearMeetEntitlements();
parent::tearDown();
}
diff --git a/src/tests/Browser/Components/SubscriptionSelect.php b/src/tests/Browser/Components/SubscriptionSelect.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Components/SubscriptionSelect.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Tests\Browser\Components;
+
+use Laravel\Dusk\Component as BaseComponent;
+use PHPUnit\Framework\Assert as PHPUnit;
+
+class SubscriptionSelect extends BaseComponent
+{
+ protected $selector;
+
+
+ public function __construct($selector)
+ {
+ $this->selector = $selector;
+ }
+
+ /**
+ * Get the root selector for the component.
+ *
+ * @return string
+ */
+ public function selector()
+ {
+ return $this->selector;
+ }
+
+ /**
+ * Assert that the browser page contains the component.
+ *
+ * @param \Laravel\Dusk\Browser $browser
+ *
+ * @return void
+ */
+ public function assert($browser)
+ {
+ $browser->assertVisible($this->selector);
+ }
+
+ /**
+ * Assert subscription record
+ */
+ public function assertSubscription($browser, int $idx, $name, $title = null, $price = null)
+ {
+ $idx += 1; // index starts with 1 in css
+ $row = "tbody tr:nth-child($idx)";
+
+ $browser->assertSeeIn("$row td.name label", $name);
+
+ if ($title !== null) {
+ $browser->assertTip("$row td.buttons button", $title);
+ }
+
+ if ($price !== null) {
+ $browser->assertSeeIn("$row td.price", $price);
+ }
+ }
+
+ /**
+ * Assert subscription state
+ */
+ public function assertSubscriptionState($browser, int $idx, bool $enabled)
+ {
+ $idx += 1; // index starts with 1 in css
+ $row = "tbody tr:nth-child($idx)";
+ $browser->{$enabled ? 'assertChecked' : 'assertNotChecked'}("$row td.selection input");
+ }
+
+ /**
+ * Enable/Disable the subscription
+ */
+ public function clickSubscription($browser, int $idx)
+ {
+ $idx += 1; // index starts with 1 in css
+ $row = "tbody tr:nth-child($idx)";
+ $browser->click("$row td.selection input");
+ }
+}
diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php
--- a/src/tests/Browser/Meet/RoomControlsTest.php
+++ b/src/tests/Browser/Meet/RoomControlsTest.php
@@ -15,7 +15,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomInterpretersTest.php b/src/tests/Browser/Meet/RoomInterpretersTest.php
--- a/src/tests/Browser/Meet/RoomInterpretersTest.php
+++ b/src/tests/Browser/Meet/RoomInterpretersTest.php
@@ -15,7 +15,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomModeratorTest.php b/src/tests/Browser/Meet/RoomModeratorTest.php
--- a/src/tests/Browser/Meet/RoomModeratorTest.php
+++ b/src/tests/Browser/Meet/RoomModeratorTest.php
@@ -17,7 +17,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomOptionsTest.php b/src/tests/Browser/Meet/RoomOptionsTest.php
--- a/src/tests/Browser/Meet/RoomOptionsTest.php
+++ b/src/tests/Browser/Meet/RoomOptionsTest.php
@@ -17,7 +17,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomQATest.php b/src/tests/Browser/Meet/RoomQATest.php
--- a/src/tests/Browser/Meet/RoomQATest.php
+++ b/src/tests/Browser/Meet/RoomQATest.php
@@ -15,7 +15,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomSetupTest.php b/src/tests/Browser/Meet/RoomSetupTest.php
--- a/src/tests/Browser/Meet/RoomSetupTest.php
+++ b/src/tests/Browser/Meet/RoomSetupTest.php
@@ -17,7 +17,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
@@ -264,9 +264,8 @@
$guest->with(new Dialog('#leave-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Room closed')
->assertSeeIn('@body', "The session has been closed by the room owner.")
- ->assertMissing('@button-cancel')
- ->assertSeeIn('@button-action', 'Close')
- ->click('@button-action');
+ ->assertSeeIn('@button-cancel', 'Close')
+ ->click('@button-cancel');
})
->assertMissing('#leave-dialog')
->waitForLocation('/login');
@@ -461,7 +460,7 @@
$guest1
->with(new Dialog('#media-setup-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Media setup')
- ->click('@button-action');
+ ->click('@button-cancel');
})
->waitFor('@session .meet-video.self')
->assertElementsCount('@session div.meet-video', 2)
@@ -495,7 +494,7 @@
->waitUntilMissing('@session .meet-subscriber.self')
->with(new Dialog('#media-setup-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Media setup')
- ->click('@button-action');
+ ->click('@button-cancel');
})
->waitFor('@session div.meet-video.self')
->assertElementsCount('@session div.meet-video', 2)
@@ -546,9 +545,8 @@
->assertVisible('form > div:nth-child(3) svg')
->assertAttribute('form > div:nth-child(3) .input-group-text', 'title', 'Camera')
->assertVisible('form > div:nth-child(3) select')
- ->assertMissing('@button-cancel')
- ->assertSeeIn('@button-action', 'Close')
- ->click('@button-action');
+ ->assertSeeIn('@button-cancel', 'Close')
+ ->click('@button-cancel');
})
->assertMissing('#media-setup-dialog')
// Test mute audio and video
@@ -556,7 +554,7 @@
->with(new Dialog('#media-setup-dialog'), function (Browser $browser) {
$browser->select('form > div:nth-child(2) select', '')
->select('form > div:nth-child(3) select', '')
- ->click('@button-action');
+ ->click('@button-cancel');
})
->assertMissing('#media-setup-dialog')
->assertVisible('@session .meet-video .status .status-audio')
diff --git a/src/tests/Browser/Meet/RoomsTest.php b/src/tests/Browser/Meet/RoomsTest.php
--- a/src/tests/Browser/Meet/RoomsTest.php
+++ b/src/tests/Browser/Meet/RoomsTest.php
@@ -2,12 +2,15 @@
namespace Tests\Browser\Meet;
-use App\Sku;
+use App\Meet\Room;
use Tests\Browser;
+use Tests\Browser\Components\SubscriptionSelect;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\Meet\Room as RoomPage;
+use Tests\Browser\Pages\Meet\RoomInfo;
+use Tests\Browser\Pages\Meet\RoomList;
use Tests\Browser\Pages\UserInfo;
use Tests\TestCaseDusk;
@@ -19,7 +22,8 @@
public function setUp(): void
{
parent::setUp();
- $this->clearMeetEntitlements();
+
+ Room::withTrashed()->whereNotIn('name', ['shared', 'john'])->forceDelete();
}
/**
@@ -27,14 +31,14 @@
*/
public function tearDown(): void
{
- $this->clearMeetEntitlements();
+ Room::withTrashed()->whereNotIn('name', ['shared', 'john'])->forceDelete();
+ $room = $this->resetTestRoom('shared', ['acl' => ['jack@kolab.org, full']]);
+
parent::tearDown();
}
/**
- * Test rooms page (unauthenticated and unauthorized)
- *
- * @group meet
+ * Test rooms page (unauthenticated)
*/
public function testRoomsUnauth(): void
{
@@ -42,58 +46,335 @@
$this->browse(function (Browser $browser) {
$browser->visit('/rooms')
->on(new Home())
- // User has no 'meet' entitlement yet, expect redirect to error page
->submitLogon('john@kolab.org', 'simple123', false)
- ->waitFor('#app > #error-page')
- ->assertSeeIn('#error-page .code', '403')
- ->assertSeeIn('#error-page .message', 'Access denied');
+ ->on(new RoomList());
});
}
/**
- * Test rooms page
+ * Test rooms list page
*
* @group meet
*/
public function testRooms(): void
{
$this->browse(function (Browser $browser) {
- $href = \config('app.url') . '/meet/john';
$john = $this->getTestUser('john@kolab.org');
- // User has no 'meet' entitlement yet
$browser->visit('/login')
->on(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
->on(new Dashboard())
- ->assertMissing('@links a.link-chat');
+ ->assertSeeIn('@links a.link-chat', 'Video chat')
+ // Test Video chat page
+ ->click('@links a.link-chat')
+ ->on(new RoomList())
+ ->whenAvailable('@table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 2)
+ ->assertElementsCount('thead th', 3)
+ ->with('tbody tr:nth-child(1)', function ($browser) {
+ $browser->assertSeeIn('td:nth-child(1) a', 'john')
+ ->assertSeeIn('td:nth-child(2) a', "Standard room")
+ ->assertVisible('td.buttons button')
+ ->assertAttribute('td.buttons button', 'title', 'Enter the room');
+ })
+ ->with('tbody tr:nth-child(2)', function ($browser) {
+ $browser->assertSeeIn('td:nth-child(1) a', 'shared')
+ ->assertSeeIn('td:nth-child(2) a', "Shared room")
+ ->assertVisible('td.buttons button')
+ ->assertAttribute('td.buttons button', 'title', 'Enter the room');
+ })
+ ->click('tbody tr:nth-child(1) button');
+ });
+
+ $newWindow = collect($browser->driver->getWindowHandles())->last();
+ $browser->driver->switchTo()->window($newWindow);
+
+ $browser->on(new RoomPage('john'))
+ // check that entering the room skips the logon form
+ ->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@login-form')
+ ->assertVisible('@setup-form')
+ ->assertSeeIn('@setup-status-message', "The room is closed. It will be open for others after you join.")
+ ->assertSeeIn('@setup-button', "JOIN")
+ ->click('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form');
+ });
+ }
- // Goto user subscriptions, and enable 'meet' subscription
- $browser->visit('/user/' . $john->id)
- ->on(new UserInfo())
- ->whenAvailable('@skus', function ($browser) {
- $browser->click('#sku-input-meet');
+ /**
+ * Test rooms create and edit and delete
+ */
+ public function testRoomCreateAndEditAndDelete(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+
+ $this->browse(function (Browser $browser) {
+ // Test room creation
+ $browser->visit(new RoomList())
+ ->assertSeeIn('button.room-new', 'Create room')
+ ->click('button.room-new')
+ ->on(new RoomInfo())
+ ->assertVisible('@intro p')
+ ->assertElementsCount('@nav li', 1)
+ ->assertSeeIn('@nav li a', 'General')
+ ->with('@general form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Description')
+ ->assertFocused('.row:nth-child(1) input')
+ ->assertSeeIn('.row:nth-child(2) label', 'Subscriptions')
+ ->with(new SubscriptionSelect('@skus'), function ($browser) {
+ $browser->assertElementsCount('tbody tr', 2)
+ ->assertSubscription(
+ 0,
+ "Standard conference room",
+ "Audio & video conference room",
+ "0,00 CHF/month"
+ )
+ ->assertSubscriptionState(0, true)
+ ->assertSubscription(
+ 1,
+ "Group conference room",
+ "Shareable audio & video conference room",
+ "0,00 CHF/month"
+ )
+ ->assertSubscriptionState(1, false)
+ ->clickSubscription(1)
+ ->assertSubscriptionState(0, false)
+ ->assertSubscriptionState(1, true);
+ })
+ ->type('.row:nth-child(1) input', 'test123');
})
- ->scrollTo('#general button[type=submit]')->pause(200)
- ->click('#general button[type=submit]')
- ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.')
- ->click('.navbar-brand')
- ->on(new Dashboard())
- ->assertSeeIn('@links a.link-chat', 'Video chat')
- // Make sure the element also exists on Dashboard page load
- ->refresh()
+ ->click('@general button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room created successfully.")
+ ->on(new RoomList())
+ ->whenAvailable('@table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 3);
+ });
+
+ $room = Room::where('description', 'test123')->first();
+
+ $this->assertTrue($room->hasSKU('group-room'));
+
+ // Test room editing
+ $browser->click("a[href=\"/room/{$room->id}\"]")
+ ->on(new RoomInfo())
+ ->assertSeeIn('.card-title', "Room: {$room->name}")
+ ->assertVisible('@intro p')
+ ->assertVisible("@intro a[href=\"/meet/{$room->name}\"]")
+ ->assertElementsCount('@nav li', 2)
+ ->assertSeeIn('@nav li:first-child a', 'General')
+ ->with('@general form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Description')
+ ->assertFocused('.row:nth-child(1) input')
+ ->type('.row:nth-child(1) input', 'test321')
+ ->assertSeeIn('.row:nth-child(2) label', 'Subscriptions')
+ ->with(new SubscriptionSelect('@skus'), function ($browser) {
+ $browser->assertElementsCount('tbody tr', 2)
+ ->assertSubscription(
+ 0,
+ "Standard conference room",
+ "Audio & video conference room",
+ "0,00 CHF/month"
+ )
+ ->assertSubscriptionState(0, false)
+ ->assertSubscription(
+ 1,
+ "Group conference room",
+ "Shareable audio & video conference room",
+ "0,00 CHF/month"
+ )
+ ->assertSubscriptionState(1, true)
+ ->clickSubscription(0)
+ ->assertSubscriptionState(0, true)
+ ->assertSubscriptionState(1, false);
+ });
+ })
+ ->click('@general button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room updated successfully.")
+ ->on(new RoomList());
+
+ $room->refresh();
+
+ $this->assertSame('test321', $room->description);
+ $this->assertFalse($room->hasSKU('group-room'));
+
+ // Test room deleting
+ $browser->visit('/room/' . $room->id)
+ ->on(new Roominfo())
+ ->assertSeeIn('button.button-delete', 'Delete room')
+ ->click('button.button-delete')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room deleted successfully.")
+ ->on(new RoomList())
+ ->whenAvailable('@table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 2);
+ });
+ });
+ }
+
+ /**
+ * Test room settings
+ */
+ public function testRoomSettings(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $john = $this->getTestUser('john@kolab.org');
+ $room = $this->getTestRoom('test', $john->wallets()->first());
+
+ // Test that there's no Moderators for non-group rooms
+ $browser->visit('/room/' . $room->id)
+ ->on(new RoomInfo())
+ ->assertSeeIn('@nav li:last-child a', 'Settings')
+ ->click('@nav li:last-child a')
+ ->with('@settings form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Password')
+ ->assertValue('.row:nth-child(1) input', '')
+ ->assertVisible('.row:nth-child(1) .form-text')
+ ->assertSeeIn('.row:nth-child(2) label', 'Locked room')
+ ->assertNotChecked('.row:nth-child(2) input')
+ ->assertVisible('.row:nth-child(2) .form-text')
+ ->assertSeeIn('.row:nth-child(3) label', 'Subscribers only')
+ ->assertNotChecked('.row:nth-child(3) input')
+ ->assertVisible('.row:nth-child(3) .form-text')
+ ->assertMissing('.row:nth-child(4)'); // no Moderators section on a standard room
+ });
+
+ $room->forceDelete();
+ $room = $this->getTestRoom('test', $john->wallets()->first(), [], [], 'group-room');
+
+ // Now we can assert and change all settings
+ $browser->visit('/room/' . $room->id)
+ ->on(new RoomInfo())
+ ->assertSeeIn('@nav li:last-child a', 'Settings')
+ ->click('@nav li:last-child a')
+ ->with('@settings form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(4) label', 'Moderators')
+ ->assertVisible('.row:nth-child(4) .form-text')
+ ->type('#acl .input-group:first-child input', 'jack')
+ ->click('#acl a.btn');
+ })
+ ->click('@settings button[type=submit]')
+ ->assertToast(Toast::TYPE_ERROR, "Form validation error")
+ ->assertSeeIn('#acl + .invalid-feedback', "The specified email address is invalid.")
+ ->with('@settings form', function ($browser) {
+ $browser->type('.row:nth-child(1) input', 'pass')
+ ->click('.row:nth-child(2) input')
+ ->click('.row:nth-child(3) input')
+ ->click('#acl .input-group:last-child a.btn')
+ ->type('#acl .input-group:first-child input', 'jack@kolab.org')
+ ->click('#acl a.btn');
+ })
+ ->click('@settings button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room configuration updated successfully.");
+
+ $config = $room->getConfig();
+
+ $this->assertSame('pass', $config['password']);
+ $this->assertSame(true, $config['locked']);
+ $this->assertSame(true, $config['nomedia']);
+ $this->assertSame(['jack@kolab.org, full'], $config['acl']);
+ });
+ }
+
+ /**
+ * Test acting as a non-controller user
+ *
+ * @group meet
+ */
+ public function testNonControllerRooms(): void
+ {
+ $jack = $this->getTestUser('jack@kolab.org');
+ $john = $this->getTestUser('john@kolab.org');
+ $room = $this->resetTestRoom('shared', [
+ 'password' => 'pass',
+ 'locked' => true,
+ 'nomedia' => true,
+ 'acl' => ['jack@kolab.org, full']
+ ]);
+
+ $this->browse(function (Browser $browser) use ($room, $jack) {
+ $browser->visit(new Home())
+ ->submitLogon('jack@kolab.org', 'simple123', true)
->on(new Dashboard())
- ->assertSeeIn('@links a.link-chat', 'Video chat');
-
- // Test Video chat page
- $browser->click('@links a.link-chat')
- ->waitFor('#meet-rooms')
- ->waitFor('.card-text a')
- ->assertSeeIn('.card-title', 'Voice & Video Conferencing')
- ->assertSeeIn('.card-text a', $href)
- ->assertAttribute('.card-text a', 'href', '/meet/john')
- ->click('.card-text a')
- ->on(new RoomPage('john'))
+ ->click('@links a.link-chat')
+ ->on(new RoomList())
+ ->assertMissing('button.room-new')
+ ->whenAvailable('@table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 2); // one shared room, one owned room
+ });
+
+ // the owned room
+ $owned = $jack->rooms()->first();
+ $browser->visit('/room/' . $owned->id)
+ ->on(new RoomInfo())
+ ->assertSeeIn('.card-title', "Room: {$owned->name}")
+ ->assertVisible('@intro p')
+ ->assertVisible("@intro a[href=\"/meet/{$owned->name}\"]")
+ ->assertMissing('button.button-delete')
+ ->assertElementsCount('@nav li', 2)
+ ->with('@general form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Description')
+ ->assertFocused('.row:nth-child(1) input')
+ ->assertSeeIn('.row:nth-child(2) label', 'Subscriptions')
+ ->with(new SubscriptionSelect('@skus'), function ($browser) {
+ $browser->assertElementsCount('tbody tr', 1)
+ ->assertSubscription(
+ 0,
+ "Standard conference room",
+ "Audio & video conference room",
+ "0,00 CHF/month"
+ )
+ ->assertSubscriptionState(0, true);
+ });
+ })
+ ->click('@nav li:last-child a')
+ ->with('@settings form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Password')
+ ->assertValue('.row:nth-child(1) input', '')
+ ->assertSeeIn('.row:nth-child(2) label', 'Locked room')
+ ->assertNotChecked('.row:nth-child(2) input')
+ ->assertSeeIn('.row:nth-child(3) label', 'Subscribers only')
+ ->assertNotChecked('.row:nth-child(3) input')
+ ->assertMissing('.row:nth-child(4)');
+ });
+
+ // Shared room
+ $browser->visit('/room/' . $room->id)
+ ->on(new RoomInfo())
+ ->assertSeeIn('.card-title', "Room: {$room->name}")
+ ->assertVisible('@intro p')
+ ->assertVisible("@intro a[href=\"/meet/{$room->name}\"]")
+ ->assertMissing('button.button-delete')
+ ->assertElementsCount('@nav li', 1)
+ // Test room settings
+ ->assertSeeIn('@nav li:last-child a', 'Settings')
+ ->with('@settings form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Password')
+ ->assertValue('.row:nth-child(1) input', 'pass')
+ ->assertSeeIn('.row:nth-child(2) label', 'Locked room')
+ ->assertChecked('.row:nth-child(2) input')
+ ->assertSeeIn('.row:nth-child(3) label', 'Subscribers only')
+ ->assertChecked('.row:nth-child(3) input')
+ ->assertMissing('.row:nth-child(4)')
+ ->type('.row:nth-child(1) input', 'pass123')
+ ->click('.row:nth-child(2) input')
+ ->click('.row:nth-child(3) input');
+ })
+ ->click('@settings button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room configuration updated successfully.");
+
+ $config = $room->getConfig();
+
+ $this->assertSame('pass123', $config['password']);
+ $this->assertSame(false, $config['locked']);
+ $this->assertSame(false, $config['nomedia']);
+ $this->assertSame(['jack@kolab.org, full'], $config['acl']);
+
+ $browser->click("@intro a[href=\"/meet/shared\"]")
+ ->on(new RoomPage('shared'))
// check that entering the room skips the logon form
->assertMissing('@toolbar')
->assertMissing('@menu')
@@ -105,7 +386,8 @@
->assertSeeIn('@setup-button', "JOIN")
->click('@setup-button')
->waitFor('@session')
- ->assertMissing('@setup-form');
+ ->assertMissing('@setup-form')
+ ->waitFor('a.meet-nickname svg.moderator');
});
}
}
diff --git a/src/tests/Browser/Pages/Meet/RoomInfo.php b/src/tests/Browser/Pages/Meet/RoomInfo.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/Meet/RoomInfo.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Tests\Browser\Pages\Meet;
+
+use Laravel\Dusk\Page;
+
+class RoomInfo 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 The browser object
+ *
+ * @return void
+ */
+ public function assert($browser)
+ {
+ $browser->waitUntilMissing('@app .app-loader');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@intro' => '#room-intro',
+ '@general' => '#general',
+ '@nav' => 'ul.nav-tabs',
+ '@settings' => '#settings',
+ '@skus' => '#room-skus table',
+ ];
+ }
+}
diff --git a/src/tests/Browser/Pages/Meet/RoomList.php b/src/tests/Browser/Pages/Meet/RoomList.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/Meet/RoomList.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Tests\Browser\Pages\Meet;
+
+use Laravel\Dusk\Page;
+
+class RoomList extends Page
+{
+ /**
+ * Get the URL for the page.
+ *
+ * @return string
+ */
+ public function url(): string
+ {
+ return '/rooms';
+ }
+
+ /**
+ * Assert that the browser is on the page.
+ *
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ *
+ * @return void
+ */
+ public function assert($browser)
+ {
+ $browser->waitForLocation($this->url())
+ ->waitUntilMissing('@app .app-loader')
+ ->assertSeeIn('@list .card-title', 'Voice & video conferencing rooms');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@list' => '#rooms-list',
+ '@table' => '#rooms-list table',
+ ];
+ }
+}
diff --git a/src/tests/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php
--- a/src/tests/Browser/Reseller/UserTest.php
+++ b/src/tests/Browser/Reseller/UserTest.php
@@ -38,7 +38,6 @@
$wallet->save();
$this->deleteTestGroup('group-test@kolab.org');
- $this->clearMeetEntitlements();
}
/**
@@ -59,7 +58,6 @@
$wallet->save();
$this->deleteTestGroup('group-test@kolab.org');
- $this->clearMeetEntitlements();
parent::tearDown();
}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -55,7 +55,6 @@
$wallet->save();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
}
/**
@@ -83,7 +82,6 @@
$wallet->save();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
parent::tearDown();
}
@@ -202,7 +200,7 @@
$browser->assertSeeIn('div.row:nth-child(8) label', 'Subscriptions')
->assertVisible('@skus.row:nth-child(8)')
->with('@skus', function ($browser) {
- $browser->assertElementsCount('tbody tr', 6)
+ $browser->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.name', 'User Mailbox')
->assertSeeIn('tbody tr:nth-child(1) td.price', '5,00 CHF/month')
@@ -252,15 +250,6 @@
'tbody tr:nth-child(5) td.buttons button',
'Two factor authentication for webmail and administration panel'
)
- // Meet SKU
- ->assertSeeIn('tbody tr:nth-child(6) td.name', 'Voice & Video Conferencing (public beta)')
- ->assertSeeIn('tbody tr:nth-child(6) td.price', '0,00 CHF/month')
- ->assertNotChecked('tbody tr:nth-child(6) td.selection input')
- ->assertEnabled('tbody tr:nth-child(6) td.selection input')
- ->assertTip(
- 'tbody tr:nth-child(6) td.buttons button',
- 'Video conferencing tool'
- )
->click('tbody tr:nth-child(4) td.selection input');
})
->assertMissing('@skus table + .hint')
@@ -289,11 +278,6 @@
->assertDialogOpened('Activesync requires Groupware Features.')
->acceptDialog()
->assertNotChecked('#sku-input-activesync')
- // Check 'meet', expect an alert
- ->click('#sku-input-meet')
- ->assertDialogOpened('Voice & Video Conferencing (public beta) requires Groupware Features.')
- ->acceptDialog()
- ->assertNotChecked('#sku-input-meet')
// Check '2FA', expect 'activesync' unchecked and readonly
->click('#sku-input-2fa')
->assertChecked('#sku-input-2fa')
@@ -640,7 +624,7 @@
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
- ->assertElementsCount('tbody tr', 6)
+ ->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '4,50 CHF/month¹')
// Storage SKU
@@ -649,7 +633,7 @@
$browser->setQuotaValue(100);
})
->assertSeeIn('tr:nth-child(2) td.price', '21,37 CHF/month¹')
- // groupware SKU
+ // Groupware SKU
->assertSeeIn('tbody tr:nth-child(3) td.price', '4,41 CHF/month¹')
// ActiveSync SKU
->assertSeeIn('tbody tr:nth-child(4) td.price', '0,00 CHF/month¹')
@@ -704,7 +688,7 @@
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
// Beta SKU
- ->assertSeeIn('tbody tr:nth-child(7) td.price', '45,09 CHF/month¹')
+ ->assertSeeIn('tbody tr:nth-child(6) td.price', '45,09 CHF/month¹')
// Storage SKU
->assertSeeIn('tr:nth-child(2) td.price', '45,00 CHF/month¹')
->with($quota_input, function (Browser $browser) {
@@ -755,7 +739,7 @@
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
- ->assertElementsCount('tbody tr', 6)
+ ->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '5,00 €/month')
// Storage SKU
diff --git a/src/tests/Feature/Console/Sku/ListUsersTest.php b/src/tests/Feature/Console/Sku/ListUsersTest.php
--- a/src/tests/Feature/Console/Sku/ListUsersTest.php
+++ b/src/tests/Feature/Console/Sku/ListUsersTest.php
@@ -34,7 +34,7 @@
{
// Warning: We're not using artisan() here, as this will not
// allow us to test "empty output" cases
- $code = \Artisan::call('sku:list-users meet');
+ $code = \Artisan::call('sku:list-users domain-registration');
$output = trim(\Artisan::output());
$this->assertSame(0, $code);
$this->assertSame('', $output);
@@ -68,21 +68,5 @@
$output = trim(\Artisan::output());
$this->assertSame(0, $code);
$this->assertSame("john@kolab.org", $output);
-
- $sku = \App\Sku::where('title', 'meet')->first();
- $user = $this->getTestUser('sku-list-users@kolabnow.com');
- $user->assignSku($sku);
-
- $code = \Artisan::call('sku:list-users meet');
- $output = trim(\Artisan::output());
- $this->assertSame(0, $code);
- $this->assertSame($user->email, $output);
-
- $user->assignSku($sku);
-
- $code = \Artisan::call('sku:list-users meet');
- $output = trim(\Artisan::output());
- $this->assertSame(0, $code);
- $this->assertSame($user->email, $output);
}
}
diff --git a/src/tests/Feature/Console/User/AssignSkuTest.php b/src/tests/Feature/Console/User/AssignSkuTest.php
--- a/src/tests/Feature/Console/User/AssignSkuTest.php
+++ b/src/tests/Feature/Console/User/AssignSkuTest.php
@@ -31,7 +31,7 @@
*/
public function testHandle(): void
{
- $sku = \App\Sku::where('title', 'meet')->first();
+ $sku = \App\Sku::withEnvTenantContext()->where('title', 'groupware')->first();
$user = $this->getTestUser('add-entitlement@kolabnow.com');
$this->artisan('user:assign-sku unknown@unknown.org ' . $sku->id)
diff --git a/src/tests/Feature/Controller/Admin/SkusTest.php b/src/tests/Feature/Controller/Admin/SkusTest.php
--- a/src/tests/Feature/Controller/Admin/SkusTest.php
+++ b/src/tests/Feature/Controller/Admin/SkusTest.php
@@ -18,7 +18,6 @@
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
}
/**
@@ -29,7 +28,6 @@
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
parent::tearDown();
}
@@ -82,7 +80,7 @@
$json = $response->json();
- $this->assertCount(10, $json);
+ $this->assertCount(11, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
@@ -118,7 +116,7 @@
$json = $response->json();
- $this->assertCount(6, $json);
+ $this->assertCount(5, $json);
// Note: Details are tested where we test API\V4\SkusController
}
}
diff --git a/src/tests/Feature/Controller/MeetTest.php b/src/tests/Feature/Controller/MeetTest.php
--- a/src/tests/Feature/Controller/MeetTest.php
+++ b/src/tests/Feature/Controller/MeetTest.php
@@ -15,56 +15,21 @@
{
parent::setUp();
- $this->clearMeetEntitlements();
$room = Room::where('name', 'john')->first();
$room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
- $this->clearMeetEntitlements();
$room = Room::where('name', 'john')->first();
$room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
parent::tearDown();
}
- /**
- * Test listing user rooms
- *
- * @group meet
- */
- public function testIndex(): void
- {
- $john = $this->getTestUser('john@kolab.org');
- $jack = $this->getTestUser('jack@kolab.org');
- Room::where('user_id', $jack->id)->delete();
-
- // Unauth access not allowed
- $response = $this->get("api/v4/meet/rooms");
- $response->assertStatus(401);
-
- // John has one room
- $response = $this->actingAs($john)->get("api/v4/meet/rooms");
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertSame(1, $json['count']);
- $this->assertCount(1, $json['list']);
- $this->assertSame('john', $json['list'][0]['name']);
-
- // Jack has no room, but it will be auto-created
- $response = $this->actingAs($jack)->get("api/v4/meet/rooms");
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertSame(1, $json['count']);
- $this->assertCount(1, $json['list']);
- $this->assertMatchesRegularExpression('/^[0-9a-z-]{11}$/', $json['list'][0]['name']);
- }
-
/**
* Test joining the room
*
@@ -78,8 +43,6 @@
$room->session_id = null;
$room->save();
- $this->assignMeetEntitlement($john);
-
// Unauth access, no session yet
$response = $this->post("api/v4/meet/rooms/{$room->name}");
$response->assertStatus(422);
@@ -203,6 +166,16 @@
$json = $response->json();
$this->assertSame(Room::ROLE_PUBLISHER & $json['role'], 0);
+
+ // Test opening the session as a sharee of a room
+ $room = Room::where('name', 'shared')->first();
+ $response = $this->actingAs($jack)->post("api/v4/meet/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(Room::ROLE_SUBSCRIBER | Room::ROLE_MODERATOR | Room::ROLE_OWNER, $json['role']);
+ $this->assertMatchesRegularExpression('|^wss?://|', $json['token']);
}
/**
@@ -219,8 +192,6 @@
$room->save();
$room->setSettings(['password' => null, 'locked' => 'true']);
- $this->assignMeetEntitlement($john);
-
// Create the session (also makes sure the owner can access a locked room)
$response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}", ['init' => 1]);
$response->assertStatus(200);
@@ -300,8 +271,6 @@
*/
public function testJoinRoomGuest(): void
{
- $this->assignMeetEntitlement('john@kolab.org');
-
// There's no easy way to logout the user in the same test after
// using actingAs(). That's why this is moved to a separate test
$room = Room::where('name', 'john')->first();
@@ -317,84 +286,6 @@
$this->assertMatchesRegularExpression('|^wss?://|', $json['token']);
}
- /**
- * Test configuring the room (session)
- *
- * @group meet
- */
- public function testSetRoomConfig(): void
- {
- $john = $this->getTestUser('john@kolab.org');
- $jack = $this->getTestUser('jack@kolab.org');
- $room = Room::where('name', 'john')->first();
-
- // Unauth access not allowed
- $response = $this->post("api/v4/meet/rooms/{$room->name}/config", []);
- $response->assertStatus(401);
-
- // Non-existing room name
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/non-existing/config", []);
- $response->assertStatus(404);
-
- // TODO: Test a room with a deleted owner
-
- // Non-owner
- $response = $this->actingAs($jack)->post("api/v4/meet/rooms/{$room->name}/config", []);
- $response->assertStatus(403);
-
- // Room owner
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}/config", []);
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertCount(2, $json);
- $this->assertSame('success', $json['status']);
- $this->assertSame("Room configuration updated successfully.", $json['message']);
-
- // Set password and room lock
- $post = ['password' => 'aaa', 'locked' => 1];
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}/config", $post);
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertCount(2, $json);
- $this->assertSame('success', $json['status']);
- $this->assertSame("Room configuration updated successfully.", $json['message']);
- $room->refresh();
- $this->assertSame('aaa', $room->getSetting('password'));
- $this->assertSame('true', $room->getSetting('locked'));
-
- // Unset password and room lock
- $post = ['password' => '', 'locked' => 0];
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}/config", $post);
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertCount(2, $json);
- $this->assertSame('success', $json['status']);
- $this->assertSame("Room configuration updated successfully.", $json['message']);
- $room->refresh();
- $this->assertSame(null, $room->getSetting('password'));
- $this->assertSame(null, $room->getSetting('locked'));
-
- // Test invalid option error
- $post = ['password' => 'eee', 'unknown' => 0];
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}/config", $post);
- $response->assertStatus(422);
-
- $json = $response->json();
-
- $this->assertCount(2, $json);
- $this->assertSame('error', $json['status']);
- $this->assertSame("Invalid room configuration option.", $json['errors']['unknown']);
-
- $room->refresh();
- $this->assertSame(null, $room->getSetting('password'));
- }
-
/**
* Test the webhook
*
@@ -402,8 +293,6 @@
*/
public function testWebhook(): void
{
- $this->assignMeetEntitlement('john@kolab.org');
-
$john = $this->getTestUser('john@kolab.org');
$room = Room::where('name', 'john')->first();
$headers = ['X-Auth-Token' => \config('meet.webhook_token')];
diff --git a/src/tests/Feature/Controller/Reseller/SkusTest.php b/src/tests/Feature/Controller/Reseller/SkusTest.php
--- a/src/tests/Feature/Controller/Reseller/SkusTest.php
+++ b/src/tests/Feature/Controller/Reseller/SkusTest.php
@@ -18,7 +18,6 @@
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
}
/**
@@ -29,7 +28,6 @@
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
parent::tearDown();
}
@@ -99,7 +97,7 @@
$json = $response->json();
- $this->assertCount(10, $json);
+ $this->assertCount(11, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
@@ -167,7 +165,7 @@
$json = $response->json();
- $this->assertCount(6, $json);
+ $this->assertCount(5, $json);
// Note: Details are tested where we test API\V4\SkusController
}
}
diff --git a/src/tests/Feature/Controller/RoomsTest.php b/src/tests/Feature/Controller/RoomsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/RoomsTest.php
@@ -0,0 +1,506 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Meet\Room;
+use Tests\TestCase;
+
+class RoomsTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ Room::withTrashed()->whereNotIn('name', ['shared', 'john'])->forceDelete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ Room::withTrashed()->whereNotIn('name', ['shared', 'john'])->forceDelete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test deleting a room (DELETE /api/v4/rooms/<room-id>)
+ */
+ public function testDestroy(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $room = $this->getTestRoom('test', $john->wallets()->first(), [], [], 'group-room');
+ $room->setConfig(['acl' => 'jack@kolab.org, full']);
+
+ // Unauth access not allowed
+ $response = $this->delete("api/v4/rooms/{$room->id}");
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->delete("api/v4/rooms/non-existing");
+ $response->assertStatus(404);
+
+ // Non-owner (sharee also can't delete the room)
+ $response = $this->actingAs($jack)->delete("api/v4/rooms/{$room->id}");
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->delete("api/v4/rooms/{$room->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room deleted successfully.", $json['message']);
+ }
+
+ /**
+ * Test listing user rooms (GET /api/v4/rooms)
+ */
+ public function testIndex(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/rooms");
+ $response->assertStatus(401);
+
+ // John has two rooms
+ $response = $this->actingAs($john)->get("api/v4/rooms");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertSame('john', $json['list'][0]['name']);
+ $this->assertSame("Standard room", $json['list'][0]['description']);
+ $this->assertSame('shared', $json['list'][1]['name']);
+ $this->assertSame("Shared room", $json['list'][1]['description']);
+
+ // Ned has no rooms, but is the John's wallet controller
+ $response = $this->actingAs($ned)->get("api/v4/rooms");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertSame('john', $json['list'][0]['name']);
+ $this->assertSame('shared', $json['list'][1]['name']);
+
+ // Jack has no rooms (one will be aot-created), but John shares one with him
+ $response = $this->actingAs($jack)->get("api/v4/rooms");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $jack_room = $jack->rooms()->first();
+ $this->assertTrue(in_array($jack_room->name, [$json['list'][0]['name'], $json['list'][1]['name']]));
+ $this->assertTrue(in_array('shared', [$json['list'][0]['name'], $json['list'][1]['name']]));
+ }
+
+ /**
+ * Test configuring the room (POST api/v4/rooms/<room-id>/config)
+ */
+ public function testSetConfig(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+ $room = $this->getTestRoom('test', $john->wallets()->first(), [], [], 'group-room');
+
+ // Unauth access not allowed
+ $response = $this->post("api/v4/rooms/{$room->name}/config", []);
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->post("api/v4/rooms/non-existing/config", []);
+ $response->assertStatus(404);
+
+ // Non-owner
+ $response = $this->actingAs($jack)->post("api/v4/rooms/{$room->name}/config", []);
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->name}/config", []);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room configuration updated successfully.", $json['message']);
+
+ // Set password and room lock
+ $post = ['password' => 'aaa', 'locked' => 1];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->name}/config", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room configuration updated successfully.", $json['message']);
+ $room->refresh();
+ $this->assertSame('aaa', $room->getSetting('password'));
+ $this->assertSame('true', $room->getSetting('locked'));
+
+ // Unset password and room lock
+ $post = ['password' => '', 'locked' => 0];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->name}/config", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room configuration updated successfully.", $json['message']);
+ $this->assertSame(null, $room->getSetting('password'));
+ $this->assertSame(null, $room->getSetting('locked'));
+
+ // Test invalid option error
+ $post = ['password' => 'eee', 'unknown' => 0];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->name}/config", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("The requested configuration parameter is not supported.", $json['errors']['unknown']);
+
+ $room->refresh();
+ $this->assertSame('eee', $room->getSetting('password'));
+
+ // Test ACL
+ $post = ['acl' => ['jack@kolab.org, full', 'test, full', 'ned@kolab.org, read-only']];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json['errors']['acl']);
+ $this->assertSame("The specified email address is invalid.", $json['errors']['acl'][1]);
+ $this->assertSame("The specified permission is invalid.", $json['errors']['acl'][2]);
+ $this->assertSame([], $room->getConfig()['acl']);
+
+ $post = ['acl' => ['jack@kolab.org, full']];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame(['jack@kolab.org, full'], $room->getConfig()['acl']);
+
+ // Acting as Jack
+ $post = ['password' => '123', 'acl' => ['joe@kolab.org, full']];
+ $response = $this->actingAs($jack)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('123', $room->getConfig()['password']);
+ $this->assertSame(['jack@kolab.org, full'], $room->getConfig()['acl']);
+
+ // Acting as Ned
+ $post = ['password' => '123456'];
+ $response = $this->actingAs($ned)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('123456', $room->getConfig()['password']);
+ }
+
+ /**
+ * Test getting a room info (GET /api/v4/rooms/<room-id>)
+ */
+ public function testShow(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+ $wallet = $john->wallets()->first();
+ $room = $this->getTestRoom(
+ 'test',
+ $wallet,
+ ['description' => 'desc'],
+ ['password' => 'pass', 'locked' => true, 'acl' => []],
+ 'group-room'
+ );
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->get("api/v4/rooms/non-existing");
+ $response->assertStatus(404);
+
+ // Non-owner
+ $response = $this->actingAs($jack)->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($room->id, $json['id']);
+ $this->assertSame('test', $json['name']);
+ $this->assertSame('desc', $json['description']);
+ $this->assertSame(false, $json['isDeleted']);
+ $this->assertTrue($json['isOwner']);
+ $this->assertTrue($json['canUpdate']);
+ $this->assertTrue($json['canDelete']);
+ $this->assertTrue($json['canShare']);
+ $this->assertCount(1, $json['skus']);
+ $this->assertSame([], $json['config']['acl']);
+ $this->assertSame('pass', $json['config']['password']);
+ $this->assertSame(true, $json['config']['locked']);
+ $this->assertSame($wallet->id, $json['wallet']['id']);
+ $this->assertSame($wallet->currency, $json['wallet']['currency']);
+ $this->assertSame($wallet->balance, $json['wallet']['balance']);
+
+ // Another wallet controller
+ $response = $this->actingAs($ned)->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($room->id, $json['id']);
+ $this->assertTrue($json['isOwner']);
+ $this->assertTrue($json['canUpdate']);
+ $this->assertTrue($json['canDelete']);
+ $this->assertTrue($json['canShare']);
+
+ // Privileged user
+ $room->setConfig(['acl' => ['jack@kolab.org, full']]);
+ $response = $this->actingAs($jack)->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($room->id, $json['id']);
+ $this->assertFalse($json['isOwner']);
+ $this->assertTrue($json['canUpdate']);
+ $this->assertFalse($json['canDelete']);
+ $this->assertFalse($json['canShare']);
+ $this->assertSame('pass', $json['config']['password']);
+ $this->assertTrue(empty($json['config']['acl']));
+ }
+
+ /**
+ * Test getting a room entitlements (GET /api/v4/rooms/<room-id>/skus)
+ */
+ public function testSkus(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+ $room = Room::where('name', 'john')->first();
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->get("api/v4/rooms/non-existing/skus");
+ $response->assertStatus(404);
+
+ // Non-owner (the room is shared with Jack, but he should not see entitlements)
+ $response = $this->actingAs($jack)->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('room', $json[0]['title']);
+ $this->assertSame(true, $json[0]['enabled']);
+ $this->assertSame('group-room', $json[1]['title']);
+ $this->assertSame(false, $json[1]['enabled']);
+
+ // Room's wallet controller, not owner
+ $response = $this->actingAs($ned)->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('room', $json[0]['title']);
+ $this->assertSame(true, $json[0]['enabled']);
+ $this->assertSame('group-room', $json[1]['title']);
+ $this->assertSame(false, $json[1]['enabled']);
+
+ // Test non-controller user, expect no group-room SKU on the list
+ $room = $this->getTestRoom('test', $jack->wallets()->first());
+
+ $response = $this->actingAs($jack)->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ $this->assertSame('room', $json[0]['title']);
+ $this->assertSame(true, $json[0]['enabled']);
+ }
+
+ /**
+ * Test creating a room (POST /api/v4/rooms)
+ */
+ public function testStore(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ // Unauth access not allowed
+ $response = $this->post("api/v4/rooms", []);
+ $response->assertStatus(401);
+
+ // Only wallet controllers can create rooms (for now)
+ $response = $this->actingAs($jack)->post("api/v4/rooms", []);
+ $response->assertStatus(403);
+
+ // Description too long
+ $post = ['description' => str_repeat('.', 192)];
+ $response = $this->actingAs($john)->post("api/v4/rooms", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("The description may not be greater than 191 characters.", $json['errors']['description'][0]);
+
+ // Successful room creation
+ $post = ['description' => 'test123'];
+ $response = $this->actingAs($john)->post("api/v4/rooms", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room created successfully.", $json['message']);
+
+ $room = Room::where('description', $post['description'])->first();
+
+ $this->assertSame($room->wallet()->id, $john->wallet()->id);
+ $this->assertSame('room', $room->entitlements()->first()->sku->title);
+
+ // Successful room creation (acting as a room controller), non-default SKU
+ $sku = \App\Sku::withObjectTenantContext($ned)->where('title', 'group-room')->first();
+ $post = ['description' => 'test456', 'skus' => [$sku->id => 1]];
+ $response = $this->actingAs($ned)->post("api/v4/rooms", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room created successfully.", $json['message']);
+
+ $room = Room::where('description', $post['description'])->first();
+
+ $this->assertSame($room->wallet()->id, $john->wallet()->id);
+ $this->assertSame($sku->id, $room->entitlements()->first()->sku_id);
+ }
+
+ /**
+ * Test updating a room (PUT /api/v4/rooms</room-id>)
+ */
+ public function testUpdate(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+ $wallet = $john->wallets()->first();
+ $room = $this->getTestRoom('test', $wallet, [], [], 'group-room');
+
+ // Unauth access not allowed
+ $response = $this->put("api/v4/rooms/{$room->id}", []);
+ $response->assertStatus(401);
+
+ // Only wallet controllers can create rooms (for now)
+ $response = $this->actingAs($jack)->put("api/v4/rooms/{$room->id}", []);
+ $response->assertStatus(403);
+
+ // Description too long
+ $post = ['description' => str_repeat('.', 192)];
+ $response = $this->actingAs($john)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("The description may not be greater than 191 characters.", $json['errors']['description'][0]);
+
+ // Successful room update (room owner)
+ $post = ['description' => '123'];
+ $response = $this->actingAs($john)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room updated successfully.", $json['message']);
+
+ $room->refresh();
+ $this->assertSame($post['description'], $room->description);
+
+ // Successful room update (acting as a room controller)
+ $post = ['description' => '456'];
+ $response = $this->actingAs($ned)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room updated successfully.", $json['message']);
+
+ $room->refresh();
+ $this->assertSame($post['description'], $room->description);
+
+ // Successful room update (acting as a sharee)
+ $room->setConfig(['acl' => 'jack@kolab.org, full']);
+ $post = ['description' => '789'];
+ $response = $this->actingAs($jack)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room updated successfully.", $json['message']);
+
+ $room->refresh();
+ $this->assertSame($post['description'], $room->description);
+
+ // Test changing the room SKU (from 'group-room' to 'room')
+ $sku = \App\Sku::withObjectTenantContext($ned)->where('title', 'room')->first();
+ $post = ['skus' => [$sku->id => 1]];
+ $response = $this->actingAs($ned)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(200);
+
+ $entitlements = $room->entitlements()->get();
+ $this->assertCount(1, $entitlements);
+ $this->assertSame($sku->id, $entitlements[0]->sku_id);
+ $this->assertNull($room->getSetting('acl'));
+ }
+}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -2,7 +2,6 @@
namespace Tests\Feature\Controller;
-use App\Http\Controllers\API\V4\SkusController;
use App\Sku;
use App\Tenant;
use Tests\TestCase;
@@ -18,7 +17,6 @@
$this->deleteTestUser('jane@kolabnow.com');
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
Sku::where('title', 'test')->delete();
}
@@ -29,7 +27,6 @@
{
$this->deleteTestUser('jane@kolabnow.com');
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
Sku::where('title', 'test')->delete();
parent::tearDown();
@@ -66,7 +63,7 @@
$json = $response->json();
- $this->assertCount(10, $json);
+ $this->assertCount(11, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
@@ -96,7 +93,6 @@
$jane->assignPackage($kolab);
$response = $this->actingAs($jane)->get("api/v4/skus?type=domain");
- $response->assertStatus(200);
$json = $response->json();
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
@@ -383,7 +383,7 @@
$json = $response->json();
- $this->assertCount(6, $json);
+ $this->assertCount(5, $json);
$this->assertSkuElement('mailbox', $json[0], [
'prio' => 100,
@@ -432,15 +432,6 @@
'forbidden' => ['Activesync'],
]);
- $this->assertSkuElement('meet', $json[5], [
- 'prio' => 50,
- 'type' => 'user',
- 'handler' => 'Meet',
- 'enabled' => false,
- 'readonly' => false,
- 'required' => ['Groupware'],
- ]);
-
// Test inclusion of beta SKUs
$sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$user->assignSku($sku);
@@ -449,9 +440,9 @@
$json = $response->json();
- $this->assertCount(7, $json);
+ $this->assertCount(6, $json);
- $this->assertSkuElement('beta', $json[6], [
+ $this->assertSkuElement('beta', $json[5], [
'prio' => 10,
'type' => 'user',
'handler' => 'Beta',
@@ -620,17 +611,11 @@
$this->assertSame(['beta'], $result['skus']);
- $user->assignSku(Sku::withEnvTenantContext()->where('title', 'meet')->first());
-
- $result = UsersController::statusInfo($user);
-
- $this->assertSame(['beta', 'meet'], $result['skus']);
-
- $user->assignSku(Sku::withEnvTenantContext()->where('title', 'meet')->first());
+ $user->assignSku(Sku::withEnvTenantContext()->where('title', 'groupware')->first());
$result = UsersController::statusInfo($user);
- $this->assertSame(['beta', 'meet'], $result['skus']);
+ $this->assertSame(['beta', 'groupware'], $result['skus']);
}
/**
diff --git a/src/tests/Feature/EntitlementTest.php b/src/tests/Feature/EntitlementTest.php
--- a/src/tests/Feature/EntitlementTest.php
+++ b/src/tests/Feature/EntitlementTest.php
@@ -171,7 +171,7 @@
}
/**
- * Test Entitlement::entitlementTitle()
+ * Test EntitleableTrait::toString()
*/
public function testEntitleableTitle(): void
{
@@ -202,24 +202,24 @@
$entitlement = Entitlement::where('wallet_id', $wallet->id)
->where('sku_id', $sku_mailbox->id)->first();
- $this->assertSame($user->email, $entitlement->entitleableTitle());
+ $this->assertSame($user->email, $entitlement->entitleable->toString());
$entitlement = Entitlement::where('wallet_id', $wallet->id)
->where('sku_id', $sku_group->id)->first();
- $this->assertSame($group->email, $entitlement->entitleableTitle());
+ $this->assertSame($group->email, $entitlement->entitleable->toString());
$entitlement = Entitlement::where('wallet_id', $wallet->id)
->where('sku_id', $sku_domain->id)->first();
- $this->assertSame($domain->namespace, $entitlement->entitleableTitle());
+ $this->assertSame($domain->namespace, $entitlement->entitleable->toString());
// Make sure it still works if the entitleable is deleted
$domain->delete();
$entitlement->refresh();
- $this->assertSame($domain->namespace, $entitlement->entitleableTitle());
+ $this->assertSame($domain->namespace, $entitlement->entitleable->toString());
$this->assertNotNull($entitlement->entitleable);
}
}
diff --git a/src/tests/Feature/MeetRoomTest.php b/src/tests/Feature/MeetRoomTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/MeetRoomTest.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Meet\Room;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class MeetRoomTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('room-test@' . \config('app.domain'));
+ Room::withTrashed()->whereNotIn('name', ['shared', 'john'])->forceDelete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('room-test@' . \config('app.domain'));
+ Room::withTrashed()->whereNotIn('name', ['shared', 'john'])->forceDelete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test room/user creation
+ */
+ public function testCreate(): void
+ {
+ Queue::fake();
+
+ // Test default room name generation
+ $room = new Room();
+ $room->save();
+
+ $this->assertMatchesRegularExpression('/^[0-9a-z]{3}-[0-9a-z]{3}-[0-9a-z]{3}$/', $room->name);
+
+ // Test keeping the specified room name
+ $room = new Room();
+ $room->name = 'test';
+ $room->save();
+
+ $this->assertSame('test', $room->name);
+ }
+
+ /**
+ * Test room/user deletion
+ */
+ public function testDelete(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('room-test@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+ $room = $this->getTestRoom('test', $wallet, [], [
+ 'password' => 'test',
+ 'acl' => ['john@kolab.org, full'],
+ ], 'group-room');
+
+ $this->assertCount(1, $room->entitlements()->get());
+ $this->assertCount(1, $room->permissions()->get());
+ $this->assertCount(1, $room->settings()->get());
+
+ // First delete the room, see if it deletes the room permissions, entitlements and settings
+ $room->delete();
+
+ $this->assertTrue($room->fresh()->trashed());
+ $this->assertCount(0, $room->entitlements()->get());
+ $this->assertCount(0, $room->permissions()->get());
+ $this->assertCount(1, $room->settings()->get());
+
+ $room->forceDelete();
+
+ $this->assertCount(0, Room::where('name', 'test')->get());
+ $this->assertCount(0, $room->settings()->get());
+
+ // Now test if deleting a user deletes the room
+ $room = $this->getTestRoom('test', $wallet, [], [
+ 'password' => 'test',
+ 'acl' => ['john@kolab.org, full'],
+ ], 'group-room');
+
+ $user->delete();
+
+ $this->assertTrue($room->fresh()->trashed());
+ $this->assertCount(0, $room->entitlements()->get());
+ $this->assertCount(0, $room->permissions()->get());
+ $this->assertCount(1, $room->settings()->get());
+ }
+
+ /**
+ * Test getConfig()/setConfig() (\App\Meet\RoomConfigTrait)
+ */
+ public function testConfig(): void
+ {
+ $room = $this->getTestRoom('test');
+ $user = $this->getTestUser('room-test@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+
+ // Test input validation (acl can be set on a group room only
+ $result = $room->setConfig($input = [
+ 'acl' => ['jack@kolab.org, full'],
+ ]);
+
+ $this->assertCount(1, $result);
+ $this->assertSame("The requested configuration parameter is not supported.", $result['acl']);
+
+ $room->entitlements()->delete();
+ $room->assignToWallet($wallet, 'group-room');
+
+ // Test input validation
+ $result = $room->setConfig($input = [
+ 'acl' => ['jack@kolab.org, read-only', 'test@unknown.org, full'],
+ ]);
+
+ $this->assertCount(2, $result['acl']);
+ $this->assertSame("The specified permission is invalid.", $result['acl'][0]);
+ $this->assertSame("The specified email address does not exist.", $result['acl'][1]);
+
+ $room->setConfig($input = [
+ 'password' => 'test-pass',
+ 'nomedia' => true,
+ 'locked' => true,
+ 'acl' => ['john@kolab.org, full']
+ ]);
+
+ $config = $room->getConfig();
+
+ $this->assertCount(4, $config);
+ $this->assertSame($input['password'], $config['password']);
+ $this->assertSame($input['nomedia'], $config['nomedia']);
+ $this->assertSame($input['locked'], $config['locked']);
+ $this->assertSame($input['acl'], $config['acl']);
+ }
+}
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -11,6 +11,9 @@
class UserTest extends TestCase
{
+ /**
+ * {@inheritDoc}
+ */
public function setUp(): void
{
parent::setUp();
@@ -26,6 +29,9 @@
$this->deleteTestDomain('UserAccountAdd.com');
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
\App\TenantSetting::truncate();
@@ -857,6 +863,14 @@
);
}
+ /**
+ * Test user deletion vs. rooms
+ */
+ public function testDeleteWithRooms(): void
+ {
+ $this->markTestIncomplete();
+ }
+
/**
* Tests for User::aliasExists()
*/
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -276,6 +276,22 @@
$this->assertTrue($bAccount->id === $aWallet->id);
}
+ /**
+ * Test Wallet::isController()
+ */
+ public function testIsController(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ $wallet = $jack->wallet();
+
+ $this->assertTrue($wallet->isController($john));
+ $this->assertTrue($wallet->isController($ned));
+ $this->assertFalse($wallet->isController($jack));
+ }
+
/**
* Verify controllers can also be removed from wallets.
*/
diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php
--- a/src/tests/TestCase.php
+++ b/src/tests/TestCase.php
@@ -8,7 +8,6 @@
abstract class TestCase extends BaseTestCase
{
use TestCaseTrait;
- use TestCaseMeetTrait;
/**
* {@inheritDoc}
diff --git a/src/tests/TestCaseDusk.php b/src/tests/TestCaseDusk.php
--- a/src/tests/TestCaseDusk.php
+++ b/src/tests/TestCaseDusk.php
@@ -10,7 +10,6 @@
abstract class TestCaseDusk extends BaseTestCase
{
use TestCaseTrait;
- use TestCaseMeetTrait;
/**
* Prepare for Dusk test execution.
diff --git a/src/tests/TestCaseMeetTrait.php b/src/tests/TestCaseMeetTrait.php
deleted file mode 100644
--- a/src/tests/TestCaseMeetTrait.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-namespace Tests;
-
-use App\Meet\Room;
-
-trait TestCaseMeetTrait
-{
- /**
- * Assign 'meet' entitlement to a user.
- *
- * @param string|\App\User $user The user
- */
- protected function assignMeetEntitlement($user): void
- {
- if (is_string($user)) {
- $user = $this->getTestUser($user);
- }
-
- $user->assignSku(\App\Sku::where('title', 'meet')->first());
- }
-
- /**
- * Removes all 'meet' entitlements from the database
- */
- protected function clearMeetEntitlements(): void
- {
- $meet_sku = \App\Sku::where('title', 'meet')->first();
- \App\Entitlement::where('sku_id', $meet_sku->id)->delete();
- }
-
- /**
- * Reset a room after tests
- */
- public function resetTestRoom($room_name = 'john'): void
- {
- $this->clearMeetEntitlements();
-
- $room = Room::where('name', $room_name)->first();
- $room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
-
- if ($room->session_id) {
- $room->session_id = null;
- $room->save();
- }
- }
-
- /**
- * Prepare a room for testing
- */
- public function setupTestRoom($room_name = 'john'): void
- {
- $this->resetTestRoom($room_name);
- $this->assignMeetEntitlement('john@kolab.org');
- }
-}
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -350,6 +350,24 @@
$resource->forceDelete();
}
+ /**
+ * Delete a test room whatever it takes.
+ *
+ * @coversNothing
+ */
+ protected function deleteTestRoom($name)
+ {
+ Queue::fake();
+
+ $room = \App\Meet\Room::withTrashed()->where('name', $name)->first();
+
+ if (!$room) {
+ return;
+ }
+
+ $room->forceDelete();
+ }
+
/**
* Delete a test shared folder whatever it takes.
*
@@ -476,6 +494,27 @@
return $resource;
}
+ /**
+ * Get Room object by name, create it if needed.
+ *
+ * @coversNothing
+ */
+ protected function getTestRoom($name, $wallet = null, $attrib = [], $config = [], $title = null)
+ {
+ $attrib['name'] = $name;
+ $room = \App\Meet\Room::create($attrib);
+
+ if ($wallet) {
+ $room->assignToWallet($wallet, $title);
+ }
+
+ if (!empty($config)) {
+ $room->setConfig($config);
+ }
+
+ return $room;
+ }
+
/**
* Get SharedFolder object by email, create it if needed.
* Skip LDAP jobs.
@@ -592,6 +631,26 @@
];
}
+ /**
+ * Reset a room after tests
+ */
+ public function resetTestRoom(string $room_name = 'john', $config = [])
+ {
+ $room = \App\Meet\Room::where('name', $room_name)->first();
+ $room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
+
+ if ($room->session_id) {
+ $room->session_id = null;
+ $room->save();
+ }
+
+ if (!empty($config)) {
+ $room->setConfig($config);
+ }
+
+ return $room;
+ }
+
protected function setUpTest()
{
$this->userPassword = \App\Utils::generatePassphrase();
diff --git a/src/tests/Unit/TransactionTest.php b/src/tests/Unit/TransactionTest.php
--- a/src/tests/Unit/TransactionTest.php
+++ b/src/tests/Unit/TransactionTest.php
@@ -132,11 +132,11 @@
$this->assertSame(13, $transactions[4]->amount);
$this->assertSame(Transaction::ENTITLEMENT_CREATED, $transactions[4]->type);
$this->assertSame(
- "test@test.com created mailbox for " . $ent->entitleableTitle(),
+ "test@test.com created mailbox for " . $ent->entitleable->toString(),
$transactions[4]->toString()
);
$this->assertSame(
- "Added mailbox for " . $ent->entitleableTitle(),
+ "Added mailbox for " . $ent->entitleable->toString(),
$transactions[4]->shortDescription()
);
@@ -144,11 +144,11 @@
$this->assertSame(14, $transactions[5]->amount);
$this->assertSame(Transaction::ENTITLEMENT_BILLED, $transactions[5]->type);
$this->assertSame(
- sprintf("%s for %s is billed at 0,14 CHF", $ent->sku->title, $ent->entitleableTitle()),
+ sprintf("%s for %s is billed at 0,14 CHF", $ent->sku->title, $ent->entitleable->toString()),
$transactions[5]->toString()
);
$this->assertSame(
- sprintf("Billed %s for %s", $ent->sku->title, $ent->entitleableTitle()),
+ sprintf("Billed %s for %s", $ent->sku->title, $ent->entitleable->toString()),
$transactions[5]->shortDescription()
);
@@ -156,11 +156,11 @@
$this->assertSame(15, $transactions[6]->amount);
$this->assertSame(Transaction::ENTITLEMENT_DELETED, $transactions[6]->type);
$this->assertSame(
- sprintf("test@test.com deleted %s for %s", $ent->sku->title, $ent->entitleableTitle()),
+ sprintf("test@test.com deleted %s for %s", $ent->sku->title, $ent->entitleable->toString()),
$transactions[6]->toString()
);
$this->assertSame(
- sprintf("Deleted %s for %s", $ent->sku->title, $ent->entitleableTitle()),
+ sprintf("Deleted %s for %s", $ent->sku->title, $ent->entitleable->toString()),
$transactions[6]->shortDescription()
);
}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 3:15 AM (1 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822365
Default Alt Text
D3584.1775186142.diff (191 KB)

Event Timeline